
# What's the Process For — API reference

**Base URL:** `https://www.whatstheprocessfor.com/api/v1`

This document is designed to be pasted directly into a Claude (or any LLM)
conversation alongside your API key. When prompted for a task — "create a
process called X", "find all my processes about Y", "publish the one about
Z" — Claude can read this reference and construct the correct HTTP calls.

## How to use this reference with Claude

1. Copy the full contents of this page (there's a "Copy as Markdown" button
   at `/developers/api`, or `curl https://www.whatstheprocessfor.com/developers/api.md`).
2. Paste it into a Claude conversation as context.
3. Tell Claude what you want to do, and give it your API key. It will
   translate natural-language intent into HTTP calls.

For a deeper integration — tool calls instead of paste-in context — install
the MCP server: see `/developers/mcp`.

## Authentication

All requests require an API key. You can pass it either way:

```
Authorization: Bearer wtpf_<your-key>
```
or
```
X-API-Key: wtpf_<your-key>
```

### Key scopes

Every key has one of two **scopes**:

- `user` — personal key, acts on the owner's personal processes (no org)
- `organization` — org key, acts in the org with the role of whoever minted
  it. A key minted by a "member" cannot do things only owners/admins can do,
  even if the key itself has the `processes:write` permission.

Minting keys:
- Any signed-in user can mint a personal key at `/account/api-keys`.
- Owners/admins mint org keys at `/org/<slug>/api-keys`.
- Members submit a request there; an owner/admin approves, and the
  plaintext is shown once to the approver to share via a secure channel.

### Permission scopes

Each key also carries one or more **permission scopes**:

| Scope                | Lets the key…                                              |
|----------------------|------------------------------------------------------------|
| `processes:read`     | List processes, fetch a process + steps                    |
| `processes:write`    | Create, update, publish, **upload images via `/uploads`**  |
| `processes:delete`   | Permanently delete a process                               |
| `workflows:read`     | List workflows, fetch a workflow + ordered processes, list per-user permissions |
| `workflows:write`    | Create / edit / reorder workflows, add or remove processes, grant or revoke per-user permissions, mark or unmark training progress |
| `workflows:delete`   | Permanently delete a workflow (cascades to its links + progress) |
| `members:read`       | List org members and pending invites                        |
| `members:write`      | Update member roles, send invites                           |
| `members:delete`     | Remove members, revoke invites                              |

Default on creation: `processes:read` + `processes:write`. `delete`,
`workflows:*`, and `members:*` are opt-in. Pass them in the `scopes` array
when creating the key (or when filing a member request for admin approval).

> **Workflows + members require an organization-scoped key.** Personal keys
> (`scope: user`) cannot manage workflows or org membership and will get
> `403 This endpoint requires an organization-scoped API key.`

A `403 Missing required scope: <scope>` response means the key you're using
doesn't carry that scope — ask the key's owner to mint a new one (or file a
request for admin approval) with the scope you need.

## Rate limits

API access is open to every plan. Throttled per plan:

| Plan        | Per minute | Per day   |
|-------------|-----------:|----------:|
| Free        | 10         | 200       |
| Trial       | 20         | 500       |
| Starter     | 30         | 2,000     |
| Team        | 60         | 10,000    |
| Business    | 300        | 50,000    |
| Enterprise  | 1,000      | unlimited |

Both windows apply. On overflow you get `429 Too Many Requests` with:

```json
{
  "error": "Rate limit exceeded",
  "window": "minute",
  "limit": 10,
  "reset_at": "2026-04-23T18:41:00.000Z",
  "retry_after_seconds": 32
}
```

…and headers:

```
Retry-After: 32
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1745433660
```

Claude, when you see a 429: wait `retry_after_seconds` (or the `Retry-After`
header) before retrying. Don't burst-retry — you'll just keep getting 429s.

## Endpoints

### `GET /me`

Required scope: any (authenticated).

Confirms the key is valid and returns who it's acting as. Great first call
from a new client to verify connectivity.

**Response 200:**

```json
{
  "key_id": "9f2e…",
  "scope": "organization",
  "user_id": "51a7…",
  "organization_id": "c0d1…",
  "organization_name": "Acme Inc",
  "organization_slug": "acme",
  "scopes": ["processes:read", "processes:write"]
}
```

For personal keys, `organization_*` fields are `null`.

### `GET /processes`

Required scope: `processes:read`.

Lists processes visible to the actor, paginated.

**Query parameters:**

| Param         | Type        | Default | Notes                                      |
|---------------|-------------|---------|--------------------------------------------|
| `limit`       | integer     | 25      | 1–100                                      |
| `cursor`      | ISO 8601    | null    | Pass `next_cursor` from the previous page  |
| `visibility`  | string      | null    | Filter: `public` / `organization` / `private` / `unlisted` |
| `q`           | string      | null    | Title search (ILIKE)                       |

**Response 200:**

```json
{
  "items": [
    {
      "id": "proc-abc…",
      "slug": "how-to-onboard-a-new-client",
      "short_id": "a1b2c3",
      "title": "How to onboard a new client",
      "visibility": "organization",
      "organization_id": "c0d1…",
      "created_by": "51a7…",
      "step_count": 8,
      "featured_image": "https://…",
      "category": "onboarding",
      "created_at": "2026-04-10T14:22:18Z",
      "updated_at": "2026-04-22T09:05:40Z",
      "url": "https://www.whatstheprocessfor.com/process/a1b2c3"
    }
  ],
  "next_cursor": "2026-04-10T14:22:18Z"
}
```

`next_cursor` is `null` when there are no more pages.

### `GET /processes/:id`

Required scope: `processes:read`.

Full process document with its steps. Returns `404` if the process doesn't
exist or `403` if the key's actor doesn't have view permission.

**Response 200:**

```json
{
  "id": "proc-abc…",
  "slug": "how-to-onboard-a-new-client",
  "short_id": "a1b2c3",
  "title": "How to onboard a new client",
  "content": "Detailed intro text…",
  "visibility": "organization",
  "organization_id": "c0d1…",
  "created_by": "51a7…",
  "step_count": 8,
  "featured_image": "https://…",
  "category": "onboarding",
  "created_at": "2026-04-10T14:22:18Z",
  "updated_at": "2026-04-22T09:05:40Z",
  "url": "https://www.whatstheprocessfor.com/process/a1b2c3",
  "steps": [
    {
      "step_number": 1,
      "step_title": "Send welcome email",
      "step_description": "Use the template at…",
      "step_image": "https://…",
      "step_video": "https://youtube.com/watch?v=…"
    }
  ]
}
```

### `POST /uploads`

Required scope: `processes:write`.

Upload an image and receive a public URL you can pass as `featured_image`
or `step_image` when creating or updating a process. This is the
recommended way to attach images you don't already host somewhere — the
process create/update endpoints only accept reachable URLs, so without
this endpoint you'd need your own image host first.

**Request:** `multipart/form-data` with a `file` field.

| Field  | Type | Notes                                                  |
|--------|------|--------------------------------------------------------|
| `file` | file | Required. JPEG, PNG, GIF, or WebP. Max 10 MB.          |

**Response 201:**

```json
{
  "url": "https://…supabase.co/storage/v1/object/public/process-images/…",
  "path": "user-id/api/1745433660-screenshot.png",
  "content_type": "image/png",
  "size_bytes": 184320
}
```

**Errors:**
- `400` — missing `file`, wrong content-type, unsupported MIME type, or > 10 MB
- `401` — missing/invalid key
- `403` — key lacks `processes:write`
- `429` — rate limit hit (uploads count against the same per-minute / per-day quota)

The returned `url` is permanent and public; safe to embed anywhere. The
upload counts against the actor's plan storage the same way an in-app
upload does.

### `POST /processes`

Required scope: `processes:write`.

Create a process. For org keys, the process belongs to the org and
defaults to `organization` visibility. For personal keys, it defaults to
`public` visibility and has no org. Override with explicit `visibility`.

**Request body:**

```json
{
  "title": "How to onboard a new client",
  "description": "Short summary shown in listings",
  "content": "Optional long-form intro text",
  "category": "onboarding",
  "visibility": "organization",
  "featured_image": "https://…",
  "steps": [
    {
      "step_title": "Send welcome email",
      "step_description": "Use the template at…",
      "step_image": "https://…",
      "step_video": "https://youtube.com/watch?v=…"
    }
  ]
}
```

Required: `title`, `steps` (1–50 items). Each step needs a `step_title`.
Any image or video URL you pass must be reachable (2xx on a HEAD request)
or the request is rejected with `400`.

**Response 201:**

```json
{
  "id": "proc-new…",
  "slug": "how-to-onboard-a-new-client",
  "short_id": "x9y8z7",
  "title": "How to onboard a new client",
  "visibility": "organization",
  "organization_id": "c0d1…",
  "step_count": 1,
  "featured_image": "https://…",
  "created_at": "2026-04-23T19:00:00Z",
  "url": "https://www.whatstheprocessfor.com/process/x9y8z7"
}
```

### `PUT /processes/:id`

Required scope: `processes:write`.

Update any of: `title`, `content`, `description`, `category`, `visibility`,
`step_count`, `featured_image`. Omitted fields are left unchanged. Requires
`edit` permission on the process (owner, org admin, or the process's
`created_by` — same rules as the web UI).

Each update creates a version snapshot, so previous states are recoverable.

**Response 200:**

```json
{ "id": "proc-new…", "updated_at": "2026-04-23T19:05:12Z" }
```

### `DELETE /processes/:id`

Required scope: `processes:delete`.

Permanent delete. Requires `full` permission on the process. There is no
undo via the API — recovery requires admin intervention. Prefer setting
`visibility: "private"` via PUT if you just want to hide something.

**Response 200:**

```json
{ "id": "proc-new…", "deleted": true }
```

### `POST /processes/:id/publish`

Required scope: `processes:write`.

Sets the process's visibility. Defaults to `public` when no body is sent.
Use this when you want a single explicit "publish" call instead of a `PUT`
with a `visibility` field. Processes are reachable at their URL the moment
they're created, so this endpoint is really just a visibility flip.

```json
{ "visibility": "public" }
```

**Response 200:**

```json
{ "id": "proc-new…", "visibility": "public", "published": true }
```

## Workflows

A **workflow** is an ordered group of processes used for training and
onboarding. Each workflow lives inside one organization, has a name +
description, holds an ordered list of processes (`workflow_processes` with
a `position` integer), and tracks per-user completion (`workflow_progress`).

All workflow endpoints require an **organization-scoped key**. Role gates
are the same as the dashboard:

- **Owner / admin** — create, edit, delete workflows; grant or revoke
  per-user permissions; invite + remove members.
- **Owner / admin / manager** — add, remove, reorder processes inside a
  workflow; mark or unmark progress for any user.
- **Any active member** — list workflows, fetch a workflow's contents,
  mark or unmark their own progress.

### `GET /workflows`

Required scope: `workflows:read`.

Lists workflows in the calling key's organization, paginated.

**Query parameters:**

| Param              | Type     | Default | Notes                                  |
|--------------------|----------|---------|----------------------------------------|
| `limit`            | integer  | 25      | 1–100                                  |
| `cursor`           | ISO 8601 | null    | Pass `next_cursor` from previous page  |
| `q`                | string   | null    | Name search (ILIKE)                    |
| `include_inactive` | boolean  | false   | Include archived (`is_active = false`) |

**Response 200:**

```json
{
  "items": [
    {
      "id": "wf-abc…",
      "organization_id": "c0d1…",
      "name": "New hire onboarding",
      "description": "Day-1 through week-2 checklist",
      "is_active": true,
      "created_by": "51a7…",
      "created_at": "2026-04-10T14:22:18Z",
      "updated_at": "2026-04-22T09:05:40Z"
    }
  ],
  "next_cursor": "2026-04-10T14:22:18Z"
}
```

### `POST /workflows`

Required scope: `workflows:write`. Owner / admin only.

```json
{
  "name": "New hire onboarding",
  "description": "Day-1 through week-2 checklist",
  "is_active": true
}
```

URLs in `name` or `description` are rejected. `is_active` defaults to `true`.

### `GET /workflows/:id`

Required scope: `workflows:read`. Returns the workflow + its ordered
processes (with a snapshot of each linked process):

```json
{
  "workflow": { "id": "wf-abc…", "name": "New hire onboarding", "...": "..." },
  "processes": [
    {
      "id": "wp-1…",
      "workflow_id": "wf-abc…",
      "process_id": "proc-1…",
      "position": 0,
      "is_required": true,
      "estimated_duration_minutes": 15,
      "added_at": "2026-04-10T14:25:00Z",
      "processes": {
        "id": "proc-1…",
        "slug": "send-welcome-email",
        "short_id": "a1b2c3",
        "title": "Send welcome email",
        "visibility": "organization",
        "status": "published",
        "step_count": 4,
        "featured_image": "https://…"
      }
    }
  ]
}
```

### `PUT /workflows/:id`

Required scope: `workflows:write`. Owner / admin only. Body accepts any of
`name`, `description`, `is_active`. Omitted fields are left unchanged.

### `DELETE /workflows/:id`

Required scope: `workflows:delete`. Owner / admin only. Cascades to
`workflow_processes` and `workflow_progress`. There is no undo.

### `POST /workflows/:id/processes`

Required scope: `workflows:write`. Owner / admin / manager. Adds a process
to the workflow. Inserts at end-of-list unless `position` is provided.

```json
{
  "process_id": "proc-1…",
  "position": 0,
  "is_required": true,
  "estimated_duration_minutes": 15
}
```

A process can only appear once in a given workflow (409 on duplicate).
Private processes that don't belong to the org cannot be added.

### `PUT /workflows/:id/processes/reorder`

Required scope: `workflows:write`. Owner / admin / manager. Pass the full
ordered list of `process_id` values; the API sets `position` to each item's
array index.

```json
{ "process_ids": ["proc-1…", "proc-3…", "proc-2…"] }
```

### `DELETE /workflows/:id/processes/:processId`

Required scope: `workflows:write`. Owner / admin / manager. Removes the
link only — the underlying process is untouched.

### `GET /workflows/:id/permissions`

Required scope: `workflows:read`. Owner / admin only.

Lists per-user permission grants on this workflow (separate from the
catch-all org-role gate, which always applies). Useful when you want to
give a `member` access to a specific workflow without elevating their org
role.

### `PUT /workflows/:id/permissions`

Required scope: `workflows:write`. Owner / admin only. Idempotent — if the
user already has a grant on this workflow it is updated.

```json
{ "user_id": "51a7…", "permission_level": "edit" }
```

`permission_level` is one of `view`, `execute`, `edit`, `full`.

### `DELETE /workflows/:id/permissions/:userId`

Required scope: `workflows:write`. Owner / admin only.

### `POST /workflows/:id/progress`

Required scope: `workflows:write`.

Marks a `process_id` complete for `user_id` inside this workflow. Members
can mark their own progress; owner / admin / manager can mark for any user
in the org. The process must already be linked to the workflow.

```json
{
  "user_id": "51a7…",
  "process_id": "proc-1…",
  "notes": "Completed during week-1 walkthrough"
}
```

Duplicate marks return 409.

### `DELETE /workflows/:id/progress`

Required scope: `workflows:write`. Same role rules as marking. Pass the
target in the body so callers can unmark idempotently:

```json
{ "user_id": "51a7…", "process_id": "proc-1…" }
```

## Members

All member endpoints require an **organization-scoped key**.

### `GET /members`

Required scope: `members:read`. Any active member can list.

```json
{
  "items": [
    {
      "id": "uo-1…",
      "user_id": "51a7…",
      "role": "admin",
      "status": "active",
      "joined_at": "2026-01-12T09:00:00Z",
      "full_name": "Alex Doe",
      "avatar_url": "https://…"
    }
  ]
}
```

### `PUT /members/:userId`

Required scope: `members:write`. Owner / admin only.

```json
{ "role": "manager" }
```

Allowed roles: `admin`, `manager`, `member`. Ownership transfer is **not**
supported via the API. Admins cannot mutate other admins or the owner. You
cannot change your own role.

### `DELETE /members/:userId`

Required scope: `members:delete`. Owner / admin only.

Removes the member from the org. Cannot remove the owner or yourself.
Admins cannot remove other admins.

### `GET /invites`

Required scope: `members:read`. Owner / admin only. Lists pending invites
(unaccepted, not yet expired or otherwise).

### `POST /invites`

Required scope: `members:write`. Owner / admin only. Creates an invite,
sends a branded invite email, and returns the invite row plus
`invite_link` so you can share it manually if delivery fails.

```json
{ "email": "new.hire@example.com", "role": "member" }
```

Returns 403 if the org's plan user-cap is already reached, 409 if a
non-expired invite already exists for that email.

### `DELETE /invites/:id`

Required scope: `members:delete`. Owner / admin only. Revokes a pending
invite by ID.

## Errors

All error responses share a consistent envelope:

```json
{ "error": "Human-readable message", "details": "…optional context…" }
```

| Status | Meaning                                                        |
|--------|----------------------------------------------------------------|
| 400    | Bad request (missing field, invalid URL, >50 steps, etc.)      |
| 401    | Missing/invalid/revoked API key                                |
| 403    | Missing scope, or actor lacks permission on the resource       |
| 404    | Resource not found                                             |
| 409    | Conflict (duplicate pending request, already-approved, etc.)   |
| 429    | Rate limit exceeded — see the `Retry-After` header             |
| 500    | Server error (please report if persistent)                     |

## Versioning

The current version is `v1`, served at `/api/v1/*`. Breaking changes would
land under `/api/v2` and leave v1 working for at least 12 months after
announcement. Follow the API changelog at `/developers#changelog`.

## Quick examples

### whoami

```bash
curl -H "Authorization: Bearer wtpf_..." \
  https://www.whatstheprocessfor.com/api/v1/me
```

### Create a process

```bash
curl -X POST \
  -H "Authorization: Bearer wtpf_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Morning standup checklist",
    "steps": [
      { "step_title": "Open Jira board" },
      { "step_title": "Each team member gives a 30-sec update" },
      { "step_title": "Flag blockers in #standup" }
    ]
  }' \
  https://www.whatstheprocessfor.com/api/v1/processes
```

### Upload an image, then attach it to a new process

```bash
# 1. Upload the image — the response gives you a public URL
UPLOAD=$(curl -s -X POST \
  -H "Authorization: Bearer wtpf_..." \
  -F "file=@/path/to/screenshot.png" \
  https://www.whatstheprocessfor.com/api/v1/uploads)

IMAGE_URL=$(echo "$UPLOAD" | jq -r .url)

# 2. Use that URL as featured_image when creating a process
curl -X POST \
  -H "Authorization: Bearer wtpf_..." \
  -H "Content-Type: application/json" \
  -d "{
    \"title\": \"Onboarding flow\",
    \"featured_image\": \"$IMAGE_URL\",
    \"steps\": [{ \"step_title\": \"Send welcome email\", \"step_image\": \"$IMAGE_URL\" }]
  }" \
  https://www.whatstheprocessfor.com/api/v1/processes
```

Claude, when a user gives you a local file path or a screenshot to attach:
upload it via `POST /uploads` first, then pass the returned `url` into
`featured_image` or `step_image`. Don't try to embed base64 — the process
endpoints only accept HTTP(S) URLs.

### Find and publish a draft

```bash
# List drafts
curl -H "Authorization: Bearer wtpf_..." \
  "https://www.whatstheprocessfor.com/api/v1/processes?visibility=private&q=standup"

# Publish by ID
curl -X POST \
  -H "Authorization: Bearer wtpf_..." \
  -H "Content-Type: application/json" \
  -d '{ "visibility": "public" }' \
  https://www.whatstheprocessfor.com/api/v1/processes/proc-abc/publish
```
