Vesicle API Reference
The /v1/vesicle/* endpoints manage the webhook inbox: issue ingest URLs, read what senders delivered, and replay a delivery into a Cell.
Base URL: https://api.tissue.systems
Authentication: Authorization: Bearer <token>, where <token> is a CLI JWT or a tok_ API token. Each route lists the scope it requires. A JWT session holds every scope implicitly; an API token carries an explicit allow-list.
Endpoints are account-scoped, so they are not nested under /v1/cells/{address} — an endpoint routinely exists before any Cell does. Only the per-Cell read view hangs off a Cell.
The same operations are available through the ribo vesicle CLI, the dashboard Inbox, and the MCP tools (vesicle_endpoint_create, vesicle_endpoint_list, vesicle_endpoint_get, vesicle_endpoint_bind, vesicle_endpoint_rotate, vesicle_endpoint_revoke, vesicle_capture_list, vesicle_capture_get, vesicle_replay).
Routes
| Method and path | Scope | Does |
|---|---|---|
POST /v1/vesicle/endpoints |
vesicle:write |
Issue an ingest URL |
GET /v1/vesicle/endpoints |
vesicle:read |
List endpoints with binding state and capture counts |
GET /v1/vesicle/endpoints/{id} |
vesicle:read |
One endpoint, including its full ingest URL |
PATCH /v1/vesicle/endpoints/{id} |
vesicle:write |
Bind, rebind, unbind, or change verify / ack |
POST /v1/vesicle/endpoints/{id}/rotate |
vesicle:write |
Issue a new id; the old one keeps capturing for a grace window |
DELETE /v1/vesicle/endpoints/{id} |
vesicle:delete |
Revoke the endpoint; captures survive to their retention |
GET /v1/vesicle/endpoints/{id}/captures |
vesicle:read |
List captures, newest first, without bodies |
GET /v1/vesicle/captures/{cid} |
vesicle:read |
One capture verbatim, redacted unless reveal=true |
POST /v1/vesicle/captures/{cid}/replay |
vesicle:write |
Redeliver one capture, returning the Cell's response |
POST /v1/vesicle/endpoints/{id}/replay |
vesicle:write |
Redeliver a range, with an optional dry run |
DELETE /v1/vesicle/captures/{cid} |
vesicle:delete |
Drop one capture |
GET /v1/cells/{address}/vesicle |
vesicle:read |
The per-Cell view: endpoints bound here and recent delivery state |
Ingest itself — POST https://api.tissue.systems/hook/{endpoint_id} — is not a /v1 route and carries no scope. It takes no Authorization header, accepts any method, and the only thing it can do is deliver. Its response codes are in the overview.
Issue an endpoint
POST /v1/vesicle/endpoints (scope vesicle:write)
Body: {name, cell?, route?, verify?, ack?}. Omit cell and route for a capture-only endpoint; both can be set later with PATCH.
curl -X POST https://api.tissue.systems/v1/vesicle/endpoints \
-H "Authorization: Bearer $TISSUE_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "soro", "ack": "immediate"}'
{
"id": "hk_9f3c8a1b4e7d2c6058a1b3d5f7092e4c",
"name": "soro",
"url": "https://api.tissue.systems/hook/hk_9f3c8a1b4e7d2c6058a1b3d5f7092e4c",
"cell": null,
"cell_address": null,
"route": null,
"verify": "none",
"ack": "immediate",
"retries": 8,
"created_at": "2026-08-11T17:04:02Z",
"rotated_from": null,
"grace_until": null,
"revoked_at": null,
"captures": { "captured": 0, "delivered": 0, "retrying": 0, "rejected": 0, "dead": 0 }
}
| Field | Values |
|---|---|
name |
your label for the endpoint, unique within the account |
cell |
the Cell name deliveries are dispatched to, or null |
route |
the path they land on, e.g. /api/posts, or null |
verify |
none, hmac:KEY, stripe:KEY, github:KEY — KEY is a vault binding on the bound Cell |
ack |
immediate (202 on receipt) or on-success (the Cell's response verbatim) |
List endpoints
GET /v1/vesicle/endpoints (scope vesicle:read)
Every endpoint in the account, each with its binding state and capture counts by state.
curl https://api.tissue.systems/v1/vesicle/endpoints \
-H "Authorization: Bearer $TISSUE_TOKEN"
{
"count": 1,
"endpoints": [
{
"id": "hk_9f3c8a1b4e7d2c6058a1b3d5f7092e4c",
"name": "soro",
"url": "https://api.tissue.systems/hook/hk_9f3c8a1b4e7d2c6058a1b3d5f7092e4c",
"cell": "santixs-new",
"cell_address": "26e9871aq7x2k",
"route": "/api/posts",
"verify": "hmac:SORO_SECRET",
"ack": "immediate",
"captures": { "captured": 3, "delivered": 10, "retrying": 0, "rejected": 1, "dead": 0 }
}
]
}
A non-zero captured count on an endpoint with route: null means deliveries are arriving and nothing is running: the sender is being told 202 while the events sit in the inbox.
Get an endpoint
GET /v1/vesicle/endpoints/{id} (scope vesicle:read)
One endpoint in the full form shown under issue an endpoint, including the ingest URL.
curl https://api.tissue.systems/v1/vesicle/endpoints/hk_9f3c8a1b... \
-H "Authorization: Bearer $TISSUE_TOKEN"
Bind, rebind, or unbind
PATCH /v1/vesicle/endpoints/{id} (scope vesicle:write)
Sets any of cell, route, verify, ack. Send "route": null to unbind and return the endpoint to capture-only.
curl -X PATCH https://api.tissue.systems/v1/vesicle/endpoints/hk_9f3c8a1b... \
-H "Authorization: Bearer $TISSUE_TOKEN" -H "Content-Type: application/json" \
-d '{"cell": "santixs-new", "route": "/api/posts"}'
Returns the updated endpoint. The Cell need not exist at bind time — the binding resolves at dispatch, so an unresolvable name is a delivery failure, not a rejected request.
Binding does not backfill. Captures taken while the endpoint was unbound stay in state captured and reach the Cell only through an explicit replay.
Rotate an endpoint
POST /v1/vesicle/endpoints/{id}/rotate (scope vesicle:write)
Issues a new hk_ id with the same name, binding and settings. The old id keeps capturing for a grace window (7 days by default) and then answers 404, so a rotation the vendor has not applied yet does not drop live deliveries.
curl -X POST https://api.tissue.systems/v1/vesicle/endpoints/hk_9f3c8a1b.../rotate \
-H "Authorization: Bearer $TISSUE_TOKEN"
{
"id": "hk_41d7be09c5a83f2617e4d0b8a9c31f56",
"name": "soro",
"url": "https://api.tissue.systems/hook/hk_41d7be09c5a83f2617e4d0b8a9c31f56",
"rotated_from": "hk_9f3c8a1b4e7d2c6058a1b3d5f7092e4c",
"grace_until": "2026-08-18T17:04:02Z"
}
Captures that arrive on the old id during the window are stored with a warning recorded against them.
Revoke an endpoint
DELETE /v1/vesicle/endpoints/{id} (scope vesicle:delete)
The URL stops answering immediately and returns 404 to any further delivery. Captures already stored survive to their normal 30-day retention. Returns 204 No Content.
curl -X DELETE https://api.tissue.systems/v1/vesicle/endpoints/hk_9f3c8a1b... \
-H "Authorization: Bearer $TISSUE_TOKEN"
List captures
GET /v1/vesicle/endpoints/{id}/captures (scope vesicle:read)
Captures for one endpoint, newest first. The list form never carries bodies.
Filters: state (captured, delivered, retrying, rejected, dead), since, until, limit.
curl "https://api.tissue.systems/v1/vesicle/endpoints/hk_9f3c8a1b.../captures?state=dead&limit=20" \
-H "Authorization: Bearer $TISSUE_TOKEN"
{
"count": 1,
"captures": [
{
"id": "cap_7b21f4a90c3e5d81b6a2c4e0",
"endpoint_id": "hk_9f3c8a1b4e7d2c6058a1b3d5f7092e4c",
"received_at": "2026-08-11T17:05:41Z",
"method": "POST", "path": "/", "size": 412,
"content_type": "application/json",
"source_ip": "203.0.113.9",
"verify": "pass",
"state": "dead",
"attempts": 8, "last_status": 502,
"next_attempt_at": null
}
]
}
verify on a capture is none when no check was configured, or pass — a signature that failed is answered 401 at ingest and never stored.
Get a capture
GET /v1/vesicle/captures/{cid} (scope vesicle:read)
One capture verbatim: headers, body, source address, verification result, and the log of delivery attempts.
curl https://api.tissue.systems/v1/vesicle/captures/cap_7b21f4a9... \
-H "Authorization: Bearer $TISSUE_TOKEN"
{
"id": "cap_7b21f4a90c3e5d81b6a2c4e0",
"endpoint_id": "hk_9f3c8a1b4e7d2c6058a1b3d5f7092e4c",
"received_at": "2026-08-11T17:05:41Z",
"method": "POST", "path": "/", "size": 412,
"content_type": "application/json",
"source_ip": "203.0.113.9",
"verify": "pass",
"state": "dead",
"attempts": 8, "last_status": 502, "next_attempt_at": null,
"headers": {
"content-type": "application/json",
"x-soro-signature": "«redacted»"
},
"body": "{\"title\":\"…\"}",
"body_encoding": "utf8",
"redacted": true,
"attempts_log": [
{ "at": "2026-08-11T17:05:41Z", "status": 502, "error": null }
]
}
Redaction is the default. Header names matching authorization, x-api-key, cookie, *-signature and *-token, and JSON body keys matching the same patterns, come back as «redacted». Add ?reveal=true to get the values intact: the response then carries "redacted": false, and the read writes an audit_events row with the action vesicle.capture.reveal.
body_encoding is utf8 for text and base64 for anything that is not valid UTF-8.
Replay one capture
POST /v1/vesicle/captures/{cid}/replay (scope vesicle:write)
Dispatches the stored request at the bound Cell once and returns what the handler did, so the caller can see the outcome without going to look for it.
curl -X POST https://api.tissue.systems/v1/vesicle/captures/cap_7b21f4a9.../replay \
-H "Authorization: Bearer $TISSUE_TOKEN"
{
"capture_id": "cap_7b21f4a90c3e5d81b6a2c4e0",
"dispatched": true,
"status": 201,
"headers": { "content-type": "application/json" },
"body": "{\"stored\":\"cap_7b21f4a90c3e5d81b6a2c4e0\",\"attempt\":1}",
"duration_ms": 42
}
The Cell receives the dispatch headers as it would for a live delivery, with the same x-tissue-vesicle-id.
Replaying at a URL of your choosing is a client-side operation: ribo vesicle replay --to <url> fetches the capture over this API and performs the POST from your own machine. There is no server-side equivalent, and there will not be one: a caller-supplied target would make the platform a request-forgery instrument with an authenticated account behind it.
Replay a range
POST /v1/vesicle/endpoints/{id}/replay (scope vesicle:write)
Body: {since?, until?, ids?, state?, dry_run?}. With dry_run: true the response reports what would be sent and nothing is dispatched.
curl -X POST https://api.tissue.systems/v1/vesicle/endpoints/hk_9f3c8a1b.../replay \
-H "Authorization: Bearer $TISSUE_TOKEN" -H "Content-Type: application/json" \
-d '{"since": "2026-08-11T00:00:00Z", "state": "captured"}'
{
"endpoint": "soro",
"matched": 12,
"dispatched": 12,
"dry_run": false,
"results": [
{ "capture_id": "cap_7b21f4a90c3e5d81b6a2c4e0", "status": 201 }
]
}
Redelivery is at-least-once and order is not guaranteed. A handler that must not act twice on one event should key on x-tissue-vesicle-id, which is stable across every attempt and replay of a capture.
Drop a capture
DELETE /v1/vesicle/captures/{cid} (scope vesicle:delete)
Removes one capture and its stored body ahead of the 30-day retention. Returns 204 No Content.
curl -X DELETE https://api.tissue.systems/v1/vesicle/captures/cap_7b21f4a9... \
-H "Authorization: Bearer $TISSUE_TOKEN"
Vesicle activity for one Cell
GET /v1/cells/{address}/vesicle (scope vesicle:read)
The per-Cell view: the endpoints bound to this Cell and the recent delivery state of each, so a Cell dashboard is complete without knowing any endpoint ids.
curl https://api.tissue.systems/v1/cells/26e9871aq7x2k/vesicle \
-H "Authorization: Bearer $TISSUE_TOKEN"
{
"cell": "santixs-new",
"cell_address": "26e9871aq7x2k",
"endpoints": [
{
"id": "hk_9f3c8a1b4e7d2c6058a1b3d5f7092e4c",
"name": "soro",
"route": "/api/posts",
"captures": { "captured": 3, "delivered": 10, "retrying": 0, "rejected": 1, "dead": 0 },
"last_delivery_at": "2026-08-11T17:05:41Z"
}
]
}
See also
- Vesicle Overview: endpoints, capture states, retries, replay, and a worked integration
- ribo CLI Reference: the same operations from the terminal
- REST API: the full
/v1API and the scope model