# API reference

> A long, field-heavy reference page for a fictional HTTP API.

This reference documents a fictional project management API. It exists to show how dense reference material reads: many sections, many fields, and code in several languages.

## Overview

All requests go to `https://api.example.com/v1`. Requests and responses use JSON. Timestamps are ISO 8601 strings in UTC.

<Badge variant="note">v1</Badge> is the current version. Breaking changes ship under a new version prefix.

## Authentication

Authenticate with a bearer token in the `Authorization` header. Create tokens in your account settings.

<CodeGroup sync="lang">
```bash [curl]
curl https://api.example.com/v1/projects \
  -H "Authorization: Bearer $TOKEN"
```
```ts [TypeScript]
const response = await fetch('https://api.example.com/v1/projects', {
  headers: { Authorization: `Bearer ${token}` },
});
```
```python [Python]
import requests

response = requests.get(
    "https://api.example.com/v1/projects",
    headers={"Authorization": f"Bearer {token}"},
)
```
</CodeGroup>

<Callout type="caution">
  Tokens grant full access to your account. Never commit them or expose them in client-side code.
</Callout>

## Errors

Errors return a non-2xx status and a JSON body with a machine-readable `code`.

```json
{
  "error": {
    "code": "project_not_found",
    "message": "No project with id prj_123 exists."
  }
}
```

| Status | Meaning |
| --- | --- |
| `400` | The request body or query is invalid |
| `401` | The token is missing or expired |
| `403` | The token lacks permission for this resource |
| `404` | The resource does not exist |
| `429` | Too many requests, see [rate limits](#rate-limits) |

## Pagination

List endpoints return at most `limit` items and a `next` cursor. Pass the cursor to get the next page.

<FieldGroup>
  <Field name="limit" type="integer" default="20">Items per page, between 1 and 100.</Field>
  <Field name="cursor" type="string">The `next` value from the previous response.</Field>
</FieldGroup>

## Rate limits

Each token may make 600 requests per minute. Responses include the remaining budget.

<FieldGroup>
  <Field name="X-RateLimit-Limit" type="header">Requests allowed per window.</Field>
  <Field name="X-RateLimit-Remaining" type="header">Requests left in the current window.</Field>
  <Field name="X-RateLimit-Reset" type="header">Unix time when the window resets.</Field>
</FieldGroup>

## Projects

A project groups tasks and members.

### The project object

<FieldGroup>
  <Field name="id" type="string">Unique identifier, prefixed with `prj_`.</Field>
  <Field name="name" type="string">Display name, up to 80 characters.</Field>
  <Field name="description" type="string | null">Optional Markdown description.</Field>
  <Field name="status" type="'active' | 'archived'">Archived projects are read-only.</Field>
  <Field name="created_at" type="string">Creation time.</Field>
</FieldGroup>

### List projects

<Badge>GET</Badge> `/projects`

```bash
curl https://api.example.com/v1/projects?limit=2 \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "data": [
    { "id": "prj_1", "name": "Website", "status": "active" },
    { "id": "prj_2", "name": "Mobile app", "status": "active" }
  ],
  "next": "cur_9f2"
}
```

### Create a project

<Badge variant="tip">POST</Badge> `/projects`

<FieldGroup>
  <Field name="name" type="string" required>Display name.</Field>
  <Field name="description" type="string">Optional Markdown description.</Field>
</FieldGroup>

```bash
curl https://api.example.com/v1/projects \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Docs" }'
```

### Archive a project

<Badge variant="warning">POST</Badge> `/projects/:id/archive`

Archiving is reversible. Archived projects keep their tasks but reject writes.

## Tasks

A task belongs to exactly one project.

### The task object

<FieldGroup>
  <Field name="id" type="string">Unique identifier, prefixed with `tsk_`.</Field>
  <Field name="project_id" type="string">Owning project.</Field>
  <Field name="title" type="string">Short summary.</Field>
  <Field name="assignee_id" type="string | null">Member responsible for the task.</Field>
  <Field name="due_at" type="string | null">Due date.</Field>
  <Field name="state" type="'todo' | 'doing' | 'done'" default="'todo'">Workflow state.</Field>
</FieldGroup>

### List tasks

<Badge>GET</Badge> `/projects/:id/tasks`

<FieldGroup>
  <Field name="state" type="'todo' | 'doing' | 'done'">Filter by state.</Field>
  <Field name="assignee_id" type="string">Filter by assignee.</Field>
</FieldGroup>

### Update a task

<Badge variant="note">PATCH</Badge> `/tasks/:id`

Send only the fields you want to change.

```ts
await fetch(`https://api.example.com/v1/tasks/${id}`, {
  method: 'PATCH',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ state: 'done' }),
});
```

### Delete a task

<Badge variant="caution">DELETE</Badge> `/tasks/:id`

Deletion is permanent.

## Members

Members are people with access to a project.

### The member object

<FieldGroup>
  <Field name="id" type="string">Unique identifier, prefixed with `mem_`.</Field>
  <Field name="email" type="string">Login email.</Field>
  <Field name="role" type="'owner' | 'editor' | 'viewer'">Permission level.</Field>
</FieldGroup>

### Invite a member

<Badge variant="tip">POST</Badge> `/projects/:id/members`

<FieldGroup>
  <Field name="email" type="string" required>Invitee's email.</Field>
  <Field name="role" type="'editor' | 'viewer'" default="'viewer'">Initial role.</Field>
</FieldGroup>

## Webhooks

Webhooks send a `POST` request to your URL when something changes.

### Events

| Event | Sent when |
| --- | --- |
| `project.created` | A project is created |
| `project.archived` | A project is archived |
| `task.created` | A task is created |
| `task.updated` | Any task field changes |
| `member.invited` | A member is invited |

### Verifying signatures

Every delivery includes an `X-Signature` header: an HMAC-SHA256 of the raw body with your webhook secret.

<CodeCollapse>
```ts [verify.ts]
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifySignature(body: string, signature: string, secret: string) {
  const expected = createHmac('sha256', secret).update(body).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && timingSafeEqual(a, b);
}

export async function handleWebhook(request: Request, secret: string) {
  const body = await request.text();
  const signature = request.headers.get('X-Signature') ?? '';

  if (!verifySignature(body, signature, secret)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(body);
  console.log(`Received ${event.type}`);
  return new Response(null, { status: 204 });
}
```
</CodeCollapse>

### Retries

Failed deliveries are retried with exponential backoff for up to 24 hours. Respond with any 2xx status to acknowledge.

## Changelog

### 2026-09-01

- Added `due_at` to tasks.
- `limit` now accepts up to 100.

### 2026-06-15

- Added webhooks.
- Deprecated the `owner` query parameter on `/projects`. <Badge variant="warning">Deprecated</Badge>

### 2026-03-02

- Initial release.
