# Status callbacks: report progress on a theme

> How the app that received a theme reports planned, in progress or shipped back to Camelus, and what each status changes.

When Camelus sends a theme to a [destination](/docs/destinations), the app on the other end can report what happened to it: planned, in progress, shipped. The report shows on the theme under **Linked work**, can change the theme's status, and a "shipped" report drafts an email to the people who asked for it.

## Report a status

Every theme payload carries a `callback` block:

```json
"callback": { "url": "https://api.camelus.si/v1/callbacks/k97d…", "token": "cbt_…" }
```

POST to that URL with the token as a bearer token:

```bash
curl https://api.camelus.si/v1/callbacks/k97d… \
  -H "Authorization: Bearer cbt_…" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "shipped",
    "external_id": "LIN-412",
    "external_url": "https://linear.app/acme/issue/LIN-412",
    "deploy_url": "https://habits.app",
    "note": "Merged in PR #88"
  }'
```

| Field | Type | Notes |
| --- | --- | --- |
| `status` | `planned` · `in_progress` · `shipped` · `declined` · `failed`, required | See below. |
| `external_id` | string | Shown on the theme, e.g. an issue number. |
| `external_url` | URL | A link to the issue or pull request. |
| `deploy_url` | URL | Where the change is live, for `shipped`. It goes into the email to requesters. |
| `note` | string | Plain text, up to 500 characters. Shown on the theme's timeline. |

The answer is `200` with the theme's status: `{ "ok": true, "theme_status": "shipped" }`.

## What each status does

- `planned`, `in_progress`, `shipped` move the theme forward. A callback never moves a theme back (a late `in_progress` after `shipped` is recorded but doesn't change the status), and never reopens a declined or closed theme.
- `shipped` also drafts a **shipped** email to the requesters who asked to hear back. Someone reviews and sends it from the theme, unless **auto-send on shipped** is on in Settings → Loop messages.
- `declined` adds a note and asks on the theme page whether to decline the theme too. Camelus never declines a theme on its own.
- `failed` adds a note and changes nothing else.

## Details

- The token works for one theme and one destination. Deleting the destination revokes its tokens.
- Instead of the token you can sign the request like an outbound webhook: `Camelus-Timestamp` and `Camelus-Signature: v1=<hex HMAC-SHA256 of "{timestamp}.{raw body}">` with the destination's signing secret. Timestamps older than 5 minutes are refused.
- Repeats are safe: the same status with the same `external_id` is applied once, and answered with `"duplicate": true`.
- If the theme was merged since, the report goes to the theme it was merged into.
- At most 60 callbacks a minute per theme and destination.
