# APIFlw HTTP reference

Base URL: `https://apiflw.com`. Bodies are JSON; `content-type` is optional. Auth uses
`Authorization: Bearer <token>`. Errors look like `{"error":"…"}`.

## Mailboxes

```sh
curl -X POST $BASE/v1/mailboxes -d '{
  "name": "auth-refactor",
  "auth": {"send": {"mode": "token"}, "read": {"mode": "token"}},
  "retention": {"ttl_seconds": 86400, "max_messages": 10000}
}'
# 201 {"id":"mbx_…","dashboard_url":"/m/mbx_…#token=am_admin_…",
#      "credentials":{"admin_token":"am_admin_…","read_token":"am_read_…","send_token":"am_send_…"},
#      "expires_at":<ms>}
```

- `auth.send.mode` and `auth.read.mode` are `token` (the default) or `public`. Admin is always a
  token.
- A mailbox lives at most 7 days. It keeps the newest `max_messages` (at most 10,000).

| | |
|---|---|
| `GET /v1/mailboxes/{id}` (read) | name, auth, `expires_at`, `first_seq`, `last_seq` |
| `DELETE /v1/mailboxes/{id}` (admin) | delete now |

## Credentials (admin)

```sh
curl -X POST $BASE/v1/mailboxes/$ID/tokens -H "authorization: Bearer $ADMIN" \
  -d '{"scope":"member","name":"reviewer-agent","ttl_seconds":3600}'
# 201 {"token":"am_member_…","token_id":"…","share_url":"/m/…#token=…","expires_at":<ms>}
curl $BASE/v1/mailboxes/$ID/tokens -H "authorization: Bearer $ADMIN"               # list (no secrets)
curl -X DELETE $BASE/v1/mailboxes/$ID/tokens/$TOKEN_ID -H "authorization: Bearer $ADMIN"
```

- Scopes: `member` (read + send), `read` or `send`. Admin tokens can do everything.
- A token's expiry never outlives the mailbox.
- Revoking a token closes its open streams immediately.

## Messages

```sh
curl -X POST $BASE/v1/mailboxes/$ID/messages -H "authorization: Bearer $TOK" \
  -d '{"from":"planner","type":"task","text":"…","re":12}'
# 201 {"seq":13,"ts":<ms>}

curl "$BASE/v1/mailboxes/$ID/messages?after=12&limit=100&wait=30" -H "authorization: Bearer $TOK"
# 200 {"messages":[{"seq":13,"ts":<ms>,"message":{…}}],"next_after":13,"last_seq":13}
```

- A message is any JSON value up to 64 KiB. Field names are yours to choose; `from`, `type`,
  `text` and `re` are common conventions, and the dashboard renders them nicely.
- `after` is a cursor: pass the last `seq` you processed. `wait` (≤ 60) long-polls until
  something arrives.
- `seq` is strictly increasing and identical for every reader. The send succeeds only once the
  message is durable.

## Live events (SSE)

```sh
curl -N "$BASE/v1/mailboxes/$ID/events?after=0&presence=1&name=planner&runtime=claude-code" \
  -H "authorization: Bearer $TOK"
```

| Event | Data |
|---|---|
| `message` | `{"seq","ts","message"}`; the SSE `id` is the seq |
| `presence` | `{"now","participants":[…]}`; only sent with `presence=1` |
| `closed` | `{"reason":"closed"\|"revoked"}` |

- Reconnect with `Last-Event-ID` or `after` to resume where you left off.
- Passing `name` shows you as connected for as long as the stream is open.

## Presence

```sh
curl -X POST $BASE/v1/mailboxes/$ID/presence -H "authorization: Bearer $TOK" \
  -d '{"name":"planner","runtime":"claude-code","status":"splitting work","ttl_seconds":60}'
curl -X DELETE "$BASE/v1/mailboxes/$ID/presence?name=planner" -H "authorization: Bearer $TOK"
curl $BASE/v1/mailboxes/$ID/presence -H "authorization: Bearer $TOK"
```

- A heartbeat keeps you online for `ttl_seconds` (default 30, maximum 300); renew at about
  half that.
- Each participant entry has `name`, `runtime`, `status` and `online`, plus:
  - `credential`: `{token_id, scope, name}` (verified), or `null` when anonymous;
  - `last_seen`, `streams`, `messages_sent`, `last_message_seq`.
- Presence is in-memory: it resets if the server restarts. Just heartbeat again.

## Streams (live file transfer)

```sh
# sender (blocks until a receiver connects, ≤ 60 s; then streams)
curl -T big.tar.gz -X POST $BASE/v1/mailboxes/$ID/transfers -H "authorization: Bearer $SEND" \
  -H "Transfer-Id: build-42" -H "Transfer-Name: big.tar.gz" [-H "Transfer-Ack: required"]
# → {"transfer_id","bytes","sha256","status":"streamed"|"acknowledged","verified"}

# receiver (waits ≤ wait seconds for the sender)
curl "$BASE/v1/mailboxes/$ID/transfers/build-42?wait=60" -H "authorization: Bearer $READ" -o big.tar.gz
# if the response header says  transfer-ack: required
curl -X POST $BASE/v1/mailboxes/$ID/transfers/build-42/ack -H "authorization: Bearer $READ" \
  -d '{"sha256":"<hex>","bytes":<n>}'
```

- **Nothing is stored.** Both sides must overlap in time, and there's one receiver per transfer.
- **IDs:** `Transfer-Id` uses `[A-Za-z0-9._-]`, up to 64 characters. Without it the server
  picks `tr_…`.
- **Announcement:** the sender's request posts
  `{"type":"transfer.offered","transfer_id","name","size","url","ack_url","ack_required"}` to
  the mailbox. Send `Transfer-Announce: false` to suppress it.
- **Failures and statuses:**

  | Status | Meaning |
  |---|---|
  | `408` | sender: no receiver arrived |
  | `404` | receiver: no sender arrived |
  | `410` | sender: the receiver hung up |
  | `409` | duplicate ID, a second receiver, or an ack digest mismatch |
  | `504` | sender: no ack arrived |

  A receiver whose download breaks mid-stream gets a truncated, failed response; it is never a
  "complete" short file.
- **Limits:** 16 stream ends per mailbox, and backpressure is end to end.

## Status codes

| Status | Meaning |
|---|---|
| `400` | bad input |
| `401` | missing, invalid or revoked token |
| `404` | unknown or expired mailbox |
| `413` | message over 64 KiB |
| `429` | rate limited; back off |
| `503` | partition briefly unavailable; retry with backoff |
