apidocumentationmarkdown

Writing API Documentation in Markdown

A practical guide to writing API docs that developers actually read: structure, auth, endpoint reference, examples, errors, and versioning — with Markdown patterns that scale.

By mdkit Team··10 min read

API documentation is the contract between your backend and every developer who will ever integrate with it. Bad API docs cost millions of developer hours and drive integrators to competitors. Good API docs are the single highest-leverage thing a backend team can invest in after the API itself.

This post covers the structure, content, and Markdown patterns that make API docs genuinely useful.

The five questions every API doc must answer

When a developer lands on your API reference, they want to know:

  1. How do I authenticate?
  2. What endpoints exist?
  3. What parameters does each endpoint take?
  4. What does the response look like?
  5. What errors can I get, and what do they mean?

Everything else (SDKs, tutorials, guides) is secondary. Nail these five and you've built the foundation.

A scaled API reference should look roughly like this:

docs/
├── README.md                    # Overview, start here
├── authentication.md            # Auth + example token flow
├── rate-limits.md               # Limits and headers
├── errors.md                    # Status codes and error format
├── versioning.md                # How versions work, deprecation policy
├── changelog.md                 # API changelog
├── resources/
│   ├── users.md                 # Everything about /users
│   ├── orders.md                # Everything about /orders
│   └── webhooks.md              # Webhook delivery and retries
└── guides/
    ├── getting-started.md
    ├── pagination.md
    └── idempotency.md

Start simple. For a small API, a single api.md might do. Split into files when a single page gets painful to scroll.

The overview page

Your README.md or overview should be ~200–400 words and answer: who, what, where, how.

# MDKit API The MDKit API lets you convert Markdown to HTML, HTML to Markdown, and generate PDF reports programmatically. ## Base URL

https://api.mdkit.io/v1


## Quick example

```bash
curl https://api.mdkit.io/v1/convert/markdown-to-html \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"markdown": "# Hello world"}'
{ "html": "<h1>Hello world</h1>", "wordCount": 2 }

What's next


The quick example is non-negotiable. Developers skip the prose and scan for `curl`.

## Documenting an endpoint

For each endpoint, adopt a consistent template. Here's a battle-tested pattern:

````markdown
## `POST /v1/convert/markdown-to-html`

Convert a Markdown string to HTML.

### Authentication

Requires a valid API token (see [Authentication](../authentication.md)).

### Request

| Header            | Required | Description                          |
| :---------------- | :------: | :----------------------------------- |
| `Authorization`   |   Yes    | `Bearer <token>`                     |
| `Content-Type`    |   Yes    | `application/json`                   |
| `Idempotency-Key` |    No    | UUID to safely retry this request    |

#### Body

```json
{
  "markdown": "# Hello",
  "options": {
    "gfm": true,
    "breaks": false
  }
}
FieldTypeRequiredDescription
markdownstringYesSource text. Max 1 MB.
options.gfmbooleanNoEnable GitHub Flavored Markdown. Default true.
options.breaksbooleanNoRender \n as <br>. Default false.

Response

200 OK

{ "html": "<h1>Hello</h1>", "wordCount": 1, "renderTimeMs": 3 }
FieldTypeDescription
htmlstringRendered HTML.
wordCountnumberWords in the source (excluding code).
renderTimeMsnumberServer-side render time in ms.

Errors

StatusCodeMeaning
400invalid_markdownBody missing or malformed.
401unauthorizedMissing or invalid token.
413payload_too_largeMarkdown exceeded 1 MB.
429rate_limit_exceededSee Retry-After header.
500internal_errorSomething broke on our end.

Examples

curl

curl https://api.mdkit.io/v1/convert/markdown-to-html \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"markdown": "# Hello"}'

JavaScript (fetch)

const res = await fetch("https://api.mdkit.io/v1/convert/markdown-to-html", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ markdown: "# Hello" }), }); const { html } = await res.json();

Python (requests)

import requests r = requests.post( "https://api.mdkit.io/v1/convert/markdown-to-html", headers={"Authorization": f"Bearer {token}"}, json={"markdown": "# Hello"}, ) html = r.json()["html"]

Notice what's present:

- HTTP method and path as the heading.
- One-line purpose.
- Auth requirement clearly stated.
- Request schema *and* a sample request.
- Response schema *and* a sample response.
- Errors with codes (strings, not just HTTP statuses).
- At least three example languages.

Copy this as your template and fill it in for every endpoint.

## Authentication page

```markdown
# Authentication

MDKit uses bearer tokens for all API requests.

## Get a token

1. Sign in at [mdkit.io/dashboard](https://mdkit.io/dashboard).
2. Go to **Settings → API tokens**.
3. Click **Create token** and copy the value.

Tokens are 64 characters, start with `mdk_`, and are shown **once**. We
hash them on our end; there's no way to retrieve a lost token. Create
a new one if lost.

## Sending a request

Include the token in an `Authorization` header:

```bash
curl https://api.mdkit.io/v1/convert/markdown-to-html \
  -H "Authorization: Bearer mdk_abc123..."
```

## Scopes

| Scope        | Capability                               |
| :----------- | :--------------------------------------- |
| `read`       | Read your own account data               |
| `convert`    | Use conversion endpoints                 |
| `admin`      | Manage tokens and settings               |

## Token rotation

We recommend rotating tokens every 90 days. See the
[Rotation guide](./guides/rotate-tokens.md).
```

## Errors page

Centralize error handling in one page. Don't repeat error formats for every endpoint.

```markdown
# Errors

All errors return JSON with this shape:

```json
{
  "error": {
    "code": "invalid_markdown",
    "message": "Request body missing 'markdown' field.",
    "requestId": "req_abc123"
  }
}
```

| Field       | Description                                        |
| :---------- | :------------------------------------------------- |
| `code`      | Machine-readable error code (stable, safe to switch on). |
| `message`   | Human-readable. Subject to change.                 |
| `requestId` | Include in support requests.                       |

## HTTP status codes

| Code | Meaning                  |
| :--- | :----------------------- |
| 200  | Success                  |
| 400  | Malformed request        |
| 401  | Missing or invalid auth  |
| 403  | Forbidden (wrong scope)  |
| 404  | Resource not found       |
| 409  | Conflict (e.g., duplicate) |
| 413  | Payload too large        |
| 422  | Validation error         |
| 429  | Rate limited             |
| 500  | Internal server error    |
| 503  | Temporarily unavailable  |
```

## Rate limits page

```markdown
# Rate limits

Each token is limited to **100 requests per minute** by default. Larger
quotas are available on paid plans.

## Response headers

Every response includes:

| Header                  | Meaning                                    |
| :---------------------- | :----------------------------------------- |
| `X-RateLimit-Limit`     | Requests allowed per window                |
| `X-RateLimit-Remaining` | Requests remaining in the current window   |
| `X-RateLimit-Reset`     | Unix timestamp when the window resets      |

When you exceed the limit, you get a `429` with:

| Header        | Meaning                                      |
| :------------ | :------------------------------------------- |
| `Retry-After` | Seconds until you can retry                  |

## Handling rate limits

```javascript
async function callWithRetry(fn, maxRetries = 3) {
  for (let i = 0; i <= maxRetries; i++) {
    const res = await fn();
    if (res.status !== 429) return res;
    const wait = parseInt(res.headers.get("Retry-After") || "1", 10) * 1000;
    await new Promise((r) => setTimeout(r, wait));
  }
  throw new Error("Rate limited after max retries");
}
```
```

## Versioning and deprecation

Be explicit. Don't surprise your users.

```markdown
# Versioning

The MDKit API is versioned in the URL: `/v1/...`, `/v2/...`.

## Active versions

| Version | Status      | Released    | EOL       |
| :------ | :---------- | :---------- | :-------- |
| `v2`    | Stable      | 2026-01-15  | —         |
| `v1`    | Deprecated  | 2025-03-10  | 2026-07-01 |

## Deprecation policy

- **Minimum 6 months** notice before a version is discontinued.
- Responses from deprecated endpoints include `Deprecation: true` and
  `Sunset: <date>` headers.
- We send an email to all tokens that have used a deprecated endpoint
  in the last 30 days, monthly until EOL.
```

## Documentation style rules that scale

- **One purpose per endpoint section.** Don't document 3 variants in one section. Create 3 sections.
- **Bold the word after a verb when describing behavior.** "The request **creates** a new user."
- **Avoid adjectives.** "Returns a user object" is better than "Returns a well-formed user object."
- **Show, then tell.** Code block first, explanation second.
- **Copy-paste-able.** Every code example should run as-is (replace token, ready).
- **Same schema, same table layout.** Consistency reduces cognitive load. Use the request/response table pattern from the template above everywhere.
- **Explicit types.** `string`, `number`, `boolean`, `object`, `array`, `null`. Not "text" or "some JSON."
- **Nullable fields.** Mark explicitly: `string | null`.
- **Default values in tables.** Include the default for every optional field.

## Tooling

### Source of truth

If you can afford it, maintain **OpenAPI** (or JSON Schema) as the single source of truth. Tools that consume it:

- **Redoc / ReDoc** — render OpenAPI as pretty HTML docs.
- **Stoplight** — interactive API docs with a try-it-out console.
- **Postman** — imports OpenAPI, generates collections.
- **SDK generators** — openapi-generator, NSwag.

Then *generate* Markdown reference docs from OpenAPI using tools like [widdershins](https://github.com/Mermade/widdershins) or [openapi-to-md](https://www.npmjs.com/package/openapi-to-md).

### Hosting

Options:

- **In-repo, rendered on GitHub** — zero setup. Works for most APIs.
- **Static site generator** — MkDocs, Docusaurus, Nextra. Nicer UX, search, theming.
- **Dedicated platforms** — ReadMe, GitBook, Mintlify. More features, paid.

### Keeping it in sync

The single biggest failure mode for API docs is drift. Strategies:

- **PR checklist item:** "Updated docs in `/docs` if the API changed."
- **CI linting:** fail the build if a new endpoint is added without a corresponding doc file.
- **Doc review in code review:** the reviewer checks docs along with code.
- **Automated generation** from OpenAPI — if your OpenAPI is the source of truth and drives CI, it can't drift.

## Real examples to study

If you want to see excellent API docs in the wild:

- [Stripe](https://stripe.com/docs/api) — the gold standard for clarity, examples, and navigation.
- [GitHub](https://docs.github.com/en/rest) — great structure, comprehensive.
- [Twilio](https://www.twilio.com/docs/usage/api) — clear language tabs.
- [Vercel](https://vercel.com/docs/rest-api) — minimal and fast.

Read 30 minutes of any of these and you'll see patterns worth stealing.

## Summary

- Answer the five questions (auth, endpoints, params, response, errors) before anything else.
- Use a consistent per-endpoint template. Copy the one above.
- Put errors and rate limits in centralized pages — don't repeat them.
- Keep docs in the repo, updated in the same PR as the code.
- Invest in OpenAPI as a source of truth if you have more than ~5 endpoints.

Need a starting template? Our [API Documentation template](/templates/api-documentation) gives you the skeleton to fill in.

Developer time is expensive. API docs that answer the basics without friction save hours per integration. That's the ROI.

Frequently Asked Questions

Should I write API docs in Markdown or OpenAPI/Swagger?+
Both, and they serve different audiences. OpenAPI/Swagger is machine-readable — it powers SDK generation, Postman imports, and interactive explorers. Markdown is human-readable — it's what developers actually read. The best teams maintain OpenAPI as the source of truth and generate (or hand-write) a Markdown reference for human consumption.
Where should API docs live?+
In the repo. Put them in /docs or /api-docs as Markdown files alongside the code. This way, documentation changes travel with feature changes in the same PR. Hosting them on a wiki or a separate service makes them go stale within months.
How much detail should an API doc have?+
For every endpoint: method, path, purpose, auth required, parameters with types, request body schema, response schemas for success and common errors, and at least one copy-paste-able example (curl and one common SDK). Anything less is frustrating; anything more becomes noise.
Should I include rate limits in the docs?+
Yes, explicitly. Rate limits are one of the top reasons devs hit unexpected errors in production. Document per-endpoint limits, the relevant headers (X-RateLimit-Remaining, Retry-After), and what status code you return (usually 429).
How do I version API documentation?+
Version the docs with the API. If you release v1 and v2 in parallel, have /docs/v1 and /docs/v2. Link between them from a versions index page. Deprecate old versions explicitly with a banner and an EOL date.

Keep reading