Skip to content

API Documentation

Write documentation that developers actually use.

Documentation Principles

1. Show, Don't Just Tell

❌ Bad: "Use the POST method to create a user"

✅ Good:
```bash
curl -X POST https://api.example.com/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "name": "John"}'

Response:

{
  "id": 123,
  "email": "user@example.com",
  "name": "John",
  "created_at": "2024-01-15T10:30:00Z"
}
### 2. Document the Why, Not Just the What

```markdown
❌ Bad: "The `page` parameter specifies the page number"

✅ Good: "Use pagination to efficiently retrieve large datasets.
Results are returned in pages of 20 items by default.
Use `page` and `per_page` to navigate through results."

3. Cover Edge Cases

✅ Good:
**Note:** If no users match the filter criteria, an empty array is returned
with a 200 status code, not a 404.

**Rate Limiting:** This endpoint allows 100 requests per minute.
Exceeding this limit returns a 429 error with a `Retry-After` header.

Documentation Structure

API Overview

# Sartiq API

The Sartiq API provides programmatic access to photography services.

## Base URL
Production: https://api.sartiq.com/v1 Staging: https://api-staging.sartiq.com/v1
## Authentication

All requests require a Bearer token in the Authorization header:

```bash
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  https://api.sartiq.com/v1/users/me

Obtain tokens via OAuth 2.0 authentication (see Authentication section below).

Rate Limits

Plan Requests/minute Requests/day
Free 60 1,000
Pro 300 10,000
Enterprise Custom Custom

Rate limit headers are included in every response: - X-RateLimit-Limit: Maximum requests allowed - X-RateLimit-Remaining: Requests remaining - X-RateLimit-Reset: Unix timestamp when limit resets

Versioning

The API version is included in the URL path (/v1/). See versioning policy for compatibility guarantees.

### Endpoint Documentation

```markdown
## Create User

Create a new user account.
POST /users
### Request

#### Headers

| Header | Required | Description |
|--------|----------|-------------|
| Authorization | Yes | Bearer token |
| Content-Type | Yes | `application/json` |
| Idempotency-Key | No | Unique key for idempotent requests |

#### Body

```json
{
  "email": "user@example.com",
  "name": "John Doe",
  "password": "securepassword123",
  "role": "user"
}

Field Type Required Description
email string Yes Valid email address
name string Yes Full name (1-100 chars)
password string Yes Password (min 12 chars)
role string No user (default) or admin

Response

Success (201 Created)

{
  "id": 123,
  "email": "user@example.com",
  "name": "John Doe",
  "role": "user",
  "created_at": "2024-01-15T10:30:00Z"
}

Errors

Status Error Code Description
400 invalid_request Missing required fields
409 duplicate_email Email already registered
422 validation_error Invalid field values

Example

curl -X POST https://api.example.com/v1/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com",
    "name": "John Doe",
    "password": "securepassword123"
  }'

Code Examples

Python

import httpx

response = httpx.post(
    "https://api.example.com/v1/users",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "email": "john@example.com",
        "name": "John Doe",
        "password": "securepassword123",
    },
)
user = response.json()

TypeScript

const response = await fetch("https://api.example.com/v1/users", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "john@example.com",
    name: "John Doe",
    password: "securepassword123",
  }),
});
const user = await response.json();
## FastAPI Documentation Enhancement

### Docstrings for OpenAPI

```python
@app.post(
    "/users",
    response_model=User,
    status_code=201,
    summary="Create user",
    responses={
        201: {"description": "User created successfully"},
        409: {"description": "Email already exists"},
        422: {"description": "Validation error"},
    },
)
async def create_user(user: UserCreate):
    """
    Create a new user account.

    - **email**: Valid email address (must be unique)
    - **name**: Full name (1-100 characters)
    - **password**: Secure password (minimum 12 characters)

    Returns the created user object with a unique ID.
    """
    pass

Custom OpenAPI Schema

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema

    openapi_schema = get_openapi(
        title="Sartiq API",
        version="1.0.0",
        description=open("docs/api-overview.md").read(),
        routes=app.routes,
    )

    # Add security scheme
    openapi_schema["components"]["securitySchemes"] = {
        "bearerAuth": {
            "type": "http",
            "scheme": "bearer",
            "bearerFormat": "JWT",
        }
    }

    # Add servers
    openapi_schema["servers"] = [
        {"url": "https://api.sartiq.com/v1", "description": "Production"},
        {"url": "https://api-staging.sartiq.com/v1", "description": "Staging"},
    ]

    app.openapi_schema = openapi_schema
    return app.openapi_schema

app.openapi = custom_openapi

Interactive Examples

Try It Now Sections

## Try It

<div class="api-try-it">
  <form>
    <label>Email:</label>
    <input type="email" name="email" value="test@example.com">

    <label>Name:</label>
    <input type="text" name="name" value="Test User">

    <button type="submit">Send Request</button>
  </form>

  <pre class="response"></pre>
</div>

Postman/Insomnia Collections

{
  "info": {
    "name": "Sartiq API",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [{"key": "token", "value": "{{access_token}}"}]
  },
  "item": [
    {
      "name": "Users",
      "item": [
        {
          "name": "Create User",
          "request": {
            "method": "POST",
            "url": "{{base_url}}/users",
            "body": {
              "mode": "raw",
              "raw": "{\"email\": \"test@example.com\", \"name\": \"Test\"}"
            }
          }
        }
      ]
    }
  ]
}

SDK Documentation

Getting Started

# Python SDK

## Installation

```bash
pip install sartiq

Quick Start

from sartiq import Sartiq

client = Sartiq(api_key="your-api-key")

# Create a user
user = client.users.create(
    email="john@example.com",
    name="John Doe",
)

# List photos
photos = client.photos.list(user_id=user.id)

# Upload a photo
photo = client.photos.upload(
    file=open("sunset.jpg", "rb"),
    title="Sunset",
    tags=["nature", "sunset"],
)

Error Handling

from sartiq.exceptions import (
    SartiqError,
    ValidationError,
    NotFoundError,
    RateLimitError,
)

try:
    user = client.users.get(123)
except NotFoundError:
    print("User not found")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds")
except SartiqError as e:
    print(f"API error: {e.message}")
## Changelog

```markdown
# Changelog

All notable changes to the API are documented here.

## [2.0.0] - 2024-03-01

### Breaking Changes
- Removed deprecated `GET /users/list` endpoint. Use `GET /users` instead.
- Changed `name` field to `full_name` in User response.

### Added
- New `PATCH /users/{id}` endpoint for partial updates.
- Added `avatar_url` field to User response.

### Deprecated
- `GET /photos/recent` will be removed in v3. Use `GET /photos?sort=-created_at` instead.

## [1.5.0] - 2024-02-15

### Added
- Pagination support for all list endpoints.
- New `X-RateLimit-*` headers.

### Fixed
- Fixed incorrect 500 error when creating user with duplicate email (now returns 409).

Documentation Testing

Test Examples Work

import pytest
import subprocess

def extract_code_blocks(markdown: str, language: str) -> list[str]:
    """Extract code blocks of a specific language from markdown."""
    import re
    pattern = rf"```{language}\n(.*?)```"
    return re.findall(pattern, markdown, re.DOTALL)

def test_curl_examples():
    """Test that curl examples in docs are valid."""
    docs = open("docs/api.md").read()
    curl_examples = extract_code_blocks(docs, "bash")

    for example in curl_examples:
        if example.strip().startswith("curl"):
            # Validate curl syntax (dry run)
            result = subprocess.run(
                ["curl", "--dry-run"] + example.split()[1:],
                capture_output=True,
            )
            assert result.returncode == 0

def test_python_examples():
    """Test that Python examples are syntactically valid."""
    docs = open("docs/api.md").read()
    python_examples = extract_code_blocks(docs, "python")

    for example in python_examples:
        compile(example, "<string>", "exec")  # Syntax check

Keep Docs in Sync

# .github/workflows/docs.yml
name: Validate Documentation

on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Validate OpenAPI spec
        run: |
          pip install openapi-spec-validator
          openapi-spec-validator openapi.json

      - name: Test code examples
        run: pytest tests/test_docs.py

      - name: Check for broken links
        run: |
          pip install linkchecker
          linkchecker docs/

Best Practices Summary

Practice Implementation
Show real examples curl, Python, TypeScript snippets
Document all fields Type, required, constraints, defaults
Cover edge cases Empty responses, errors, limits
Provide SDKs Reduce boilerplate for clients
Version changelog Track all API changes
Test examples Ensure docs stay accurate
Interactive docs Swagger UI, try-it-now
Quick start guide Get users running in minutes