Day 17: Documentation that doesn't suck
Day 17 of 30. Today we write the docs developers will actually read.
Day 17 of 30. Today we write the docs developers will actually read.
Our current state is that we have a basic working product. Users can sign up, get API keys, even capture screenshots, and get a subscription. But the docs are currently nonexistent. There’s exactly 1 code example on the landing page and… that’s it.
We aim to deliver extremely good support, and documentation is part of this, so let’s fix this.
What developers need
When a developer lands on our docs, they want:
- Quick start - Capture a screenshot in under 2 minutes
- API reference - Every endpoint with every parameter listed
- Examples - Copy-paste code in their preferred language
- Error handling - What can go wrong, what error codes exists, and how to fix it when something goes wrong
- Concepts - How the async model works, what kind of quotas do we have, etc.
We’re writing these in order of importance, but our aim is to deliver some documentation for each of the above topics.
Interactive documentation preview
Explore the documentation structure we’re building:
Quick Start
2 min readGet your first screenshot in under 2 minutes.
curl -X POST https://api.allscreenshots.com/v1/screenshots \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' Quick Start guide
This is the most important page. It should get someone from zero to a working screenshot in the minimum possible steps.
While we aim to deliver SDKs in the near future, our current examples are focused on plain HTTP requests. But in the near future, you don’t have to use this approach, and an SDK will be provided for most popular languages.
# Quick Start
Get your first screenshot in under 2 minutes.
## 1. Get your API key
Sign up at [allscreenshots.com/signup] and copy your API key from the dashboard.
## 2. Request a screenshot
\`\`\`bash
curl -X POST https://api.allscreenshots.com/v1/screenshots \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
\`\`\`
Response:
\`\`\`json
{
"id": "scr_abc123",
"status": "pending"
}
\`\`\`
## 3. Get the result
\`\`\`bash
curl https://api.allscreenshots.com/v1/screenshots/scr_abc123 \
-H "X-API-Key: YOUR_API_KEY"
\`\`\`
Response (when complete):
\`\`\`json
{
"id": "scr_abc123",
"status": "completed",
"image_url": "https://..."
}
\`\`\`
That's it! The `image_url` contains your screenshot.
## Next steps
- [API Reference] - All endpoints and parameters
- [Device Emulation] - Mobile and tablet screenshots
- [Code Examples] - Node.js, Python, PHP, and more
Short, copy-pasteable, gets to the point.
API Reference
We document every endpoint systematically:
# API Reference
Base URL: `https://api.allscreenshots.com/v1`
Authentication: Include your API key in the `X-API-Key` header.
---
## Create Screenshot
`POST /screenshots`
Creates a new screenshot job.
### Request Body
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| url | string | Yes | - | URL to capture (must be http or https) |
| device | string | No | desktop | Device type: `desktop`, `mobile`, `tablet` |
| full_page | boolean | No | false | Capture entire scrollable page |
| width | integer | No | 1920 | Viewport width in pixels (1-4096) |
| height | integer | No | 1080 | Viewport height in pixels (1-4096) |
| format | string | No | png | Image format: `png`, `jpeg` |
| wait_for | string | No | networkidle | Wait strategy: `load`, `domcontentloaded`, `networkidle` |
### Response
**202 Accepted**
\`\`\`json
{
"id": "scr_abc123def456",
"status": "pending",
"url": "https://example.com",
"created_at": "2024-01-15T10:30:00Z"
}
\`\`\`
### Example
\`\`\`bash
curl -X POST https://api.allscreenshots.com/v1/screenshots \
-H "X-API-Key: sk_live_abc123" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"device": "mobile",
"full_page": true
}'
\`\`\`
---
## Get Screenshot
`GET /screenshots/{id}`
Retrieves the status and result of a screenshot job.
### Path Parameters
| Parameter | Description |
|-----------|-------------|
| id | The screenshot job ID (e.g., `scr_abc123`) |
### Response
**200 OK** (pending)
\`\`\`json
{
"id": "scr_abc123",
"status": "pending",
"url": "https://example.com",
"created_at": "2024-01-15T10:30:00Z"
}
\`\`\`
**200 OK** (completed)
\`\`\`json
{
"id": "scr_abc123",
"status": "completed",
"url": "https://example.com",
"image_url": "https://storage.../scr_abc123.png",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:30:03Z",
"metadata": {
"width": 1170,
"height": 2532,
"file_size": 245678,
"format": "png"
}
}
\`\`\`
...
We document every endpoint this way. Consistent structure, real examples, actual response bodies.
Code examples
Developers copy-paste code. We provide examples in popular languages:
Node.js
const SCREENSHOT_API_KEY = 'sk_live_abc123';
const API_BASE = 'https://api.allscreenshots.com/v1';
async function captureScreenshot(url, options = {}) {
// Create screenshot job
const response = await fetch(`${API_BASE}/screenshots`, {
method: 'POST',
headers: {
'X-API-Key': SCREENSHOT_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url, ...options })
});
const job = await response.json();
// Poll for completion
while (true) {
const statusResponse = await fetch(
`${API_BASE}/screenshots/${job.id}`,
{ headers: { 'X-API-Key': SCREENSHOT_API_KEY } }
);
const result = await statusResponse.json();
if (result.status === 'completed') {
return result.image_url;
}
if (result.status === 'failed') {
throw new Error(result.error.message);
}
await new Promise(r => setTimeout(r, 1000));
}
}
// Usage
const imageUrl = await captureScreenshot('https://example.com', {
device: 'mobile',
fullPage: true
});
console.log('Screenshot:', imageUrl);
Python
import requests
import time
SCREENSHOT_API_KEY = 'sk_live_abc123'
API_BASE = 'https://api.allscreenshots.com/v1'
def capture_screenshot(url, **options):
# Create screenshot job
response = requests.post(
f'{API_BASE}/screenshots',
headers={
'X-API-Key': SCREENSHOT_API_KEY,
'Content-Type': 'application/json'
},
json={'url': url, **options}
)
job = response.json()
# Poll for completion
while True:
status_response = requests.get(
f'{API_BASE}/screenshots/{job["id"]}',
headers={'X-API-Key': SCREENSHOT_API_KEY}
)
result = status_response.json()
if result['status'] == 'completed':
return result['image_url']
if result['status'] == 'failed':
raise Exception(result['error']['message'])
time.sleep(1)
# Usage
image_url = capture_screenshot('https://example.com', device='mobile')
print(f'Screenshot: {image_url}')
We also wrote examples for PHP, Ruby, and Go. It’s the same logic, just a different syntax.
Error reference
The errors need clear documentation:
# Errors
All errors follow a consistent format:
\`\`\`json
{
"error": {
"code": "error_code",
"message": "Human-readable description"
}
}
\`\`\`
## Error Codes
| Code | HTTP Status | Description | How to Fix |
|------|-------------|-------------|------------|
| invalid_url | 400 | URL is malformed or not http/https | Check the URL format |
| url_not_accessible | 400 | Could not reach the URL | Verify the site is publicly accessible |
| unauthorized | 401 | Missing or invalid API key | Check your X-API-Key header |
| rate_limited | 429 | Too many requests | Wait and retry, or upgrade your plan |
| quota_exceeded | 403 | Monthly quota exhausted | Upgrade your plan or wait for reset |
| timeout | 500 | Page took too long to load | Try a shorter wait_for strategy |
| internal_error | 500 | Something went wrong on our end | Retry, then contact support |
## Rate Limits
| Plan | Requests per hour |
|------|-------------------|
| Free | 100 |
| Pro | 1,000 |
| Scale | 10,000 |
When rate limited, the response will include:
\`\`\`json
{
"error": {
"code": "rate_limited",
"message": "Too many requests",
"retry_after": 3600
}
}
\`\`\`
## Where to host docs
Options considered:
- **GitBook** - Pretty, but costs too much money
- **Notion** - Free, but limited, and not developer-focused
- **Docusaurus** - Free, self-hosted, great DX
- **Fumadocs** - Free, self-hosted, great DX
- **Zensical** - From the makers of Material for MkDocs. Looks very elegant, but perhaps too new
- **Plain Markdown in repo** - Simple, free
This was a tough choice. In the end, we went for **Fumadocs**. It's free, generates a nice static site,
and the developer experience is good. We host it on the same VPS as our app. We picked Fumadocs over Docusaurus mostly because
we really liked the design of Fumadocs, and it seems to be mostly on par with Dcousaurus, which we've used in the past.
Let us know what you think!
## What we wrote today
- Quick start guide (1 page)
- API reference (4 endpoints documented)
- Code examples (5 languages)
- Error reference
- Concepts page (async model, quotas)
In total, we wrote about 3,000 words of documentation. It's not comprehensive, but it's enough to get started.
## Tomorrow: error handling and edge cases
On day 18 we will harden the product. What happens when sites block us? What happens when URLs are invalid? Or when we hit resource limits? Building a robust product means handling failure gracefully.
## Book of the day
**[Docs for Developers](https://www.amazon.com/Docs-Developers-Engineers-Technical-Writing/dp/1484272161?&linkCode=ll1&tag=allscreens-20) by Jared Bhatti et al.**
Written by technical writers at Google and Stripe, this book covers everything about developer documentation: planning, writing, structure, maintenance.
Key insights: developers read docs differently than other users (they scan, they copy-paste, they want to get in and out fast). Good docs have consistent structure. Examples are more important than explanations.
The chapter on API documentation directly influenced how we structured our reference pages. Worth reading if you're documenting any technical product.
---
<StatsGraph
day={17}
hoursSpent={49}
linesOfCode={3400}
revenue={0}
customers={0}
hostingCost={5.50}
achievements={[
{ label: 'Quick start guide written', completed: true },
{ label: 'API reference documented', completed: true },
{ label: '5 language examples', completed: true }
]}
/>