Files and webhooks
Uploads
An upload stages one file in a workspace for later use: as a skill package or as an asset. Send it as multipart form data with a part named file and an Idempotency-Key:
curl -X POST "$A13N_URL/api/v1/workspaces/$WORKSPACE/uploads" \
-H "Authorization: Bearer $A13N_API_KEY" -H "Idempotency-Key: report-2026-09" -F file=@report.pdf
The response gives the upload_id, filename, content type, size and SHA-256 digest. The upload ID is derived from the workspace, the caller and the idempotency key, so retrying a lost request with the same key and file returns the same upload; different content with the same key is 409 conflict (reason idempotency_key_reused). Uploads need write, are limited to objects.upload_bytes (1 MiB by default), and count against a per-principal rate limit (objects.upload_limit per objects.upload_window_seconds).
Assets
An asset is immutable content with a name, usable in messages and published by agents.
- Create from an upload with
POST /api/v1/workspaces/{workspace_id}/assetsand{"upload_id": ..., "name": ...}. The first creation returns201; repeating it with the same upload and name returns the same asset with200. An upload becomes at most one asset. - Read with
GET …/assets,GET …/assets/{asset_id}, andGET …/assets/{asset_id}/content, which returns the bytes as an attachment. - Retire with
DELETE …/assets/{asset_id}and itsIf-Match. A retired asset cannot be attached to new messages, but its content stays readable for the history that references it.
Attach an asset to a message as a content part {"type": "asset", "asset_id": "ast_..."}; see messages. The run reads the content with the submitter's access and gives it to the model as media, as text, or as a file in the run's environment; see attached files.
Agents with the assets toolset enabled can publish a file from their environment with publish_asset. The new asset records the run, attempt and tool call that produced it in source, and is bounded by objects.max_bytes.
Webhooks
A subscription delivers run lifecycle events of a workspace to an HTTPS endpoint. Managing subscriptions and reading their deliveries requires workspace admin.
curl -X POST "$A13N_URL/api/v1/workspaces/$WORKSPACE/subscriptions" \
-H "Authorization: Bearer $A13N_API_KEY" -H "Content-Type: application/json" \
-d '{"name": "Run outcomes", "url": "https://hooks.example.com/a13n",
"kinds": ["run.completed", "run.failed", "run.waiting"],
"filter": {"agent_id": "ap_..."}}'
kindsselects events:run.accepted,run.running,run.waiting,run.completed,run.failed,run.cancelled, and for attemptsrun_attempt.leased,run_attempt.running,run_attempt.succeeded,run_attempt.yielded,run_attempt.failed,run_attempt.cancelled.filteroptionally narrows events to anagent_id,session_idorthread_id.signing_secret(16–256 characters) is optional; without it the Service generates one. The secret is returned only in the creation response.PATCHchanges the name, URL, kinds, filter,enabledorsigning_secret;DELETEremoves the subscription. Both takeIf-Match. Changes affect only deliveries queued afterwards.- A workspace has at most
control.subscriptionssubscriptions (32 by default). The URL must pass the outbound policy when saved and on every delivery.
Payload and signature
Each event is a JSON POST:
{
"id": "obx_...",
"type": "run.completed",
"occurred_at": "2026-09-24T08:00:00+00:00",
"workspace_id": "ws_...",
"run": {"id": "run_...", "session_id": "sess_...", "thread_id": "thread_...", "agent_id": "ap_...",
"agent_revision_id": "apr_...", "status": "completed", "trigger": "input", "wait_reason": null, "failure": null},
"attempt": null
}
Run events carry attempt: null; attempt events add the attempt's id, number, status, start_reason, yield_reason and failure. Fetch details, such as the run's result, through the API.
Every request carries:
| Header | Value |
|---|---|
X-A13n-Delivery-Id |
The delivery ID, also the payload id. Use it to discard duplicates. |
X-A13n-Webhook-Timestamp |
Unix seconds when the request was signed. |
X-A13n-Webhook-Signature |
v1= followed by the hex HMAC-SHA256 of {timestamp}.{delivery_id}.{body}, keyed with the signing secret. |
Verify the signature over the raw body and reject old timestamps:
import hashlib, hmac
def verify(secret: str, headers, body: bytes) -> bool:
signed = f"{headers['X-A13n-Webhook-Timestamp']}.{headers['X-A13n-Delivery-Id']}.".encode() + body
expected = "v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, headers["X-A13n-Webhook-Signature"])
Delivery and retries
Delivery is at least once and not ordered. Any 2xx response within control.webhook_timeout (10 seconds by default) counts as delivered; redirects are not followed. Otherwise the delivery is retried with exponential backoff (2, 4, 8, … seconds, at most one hour apart) until it has used control.outbox_attempts attempts (12 by default), and is then marked dead.
GET …/subscriptions/{subscription_id}/deliveries lists deliveries newest first with their status (pending, delivered, dead), attempts, last error and payload. POST …/deliveries/{delivery_id}/redeliver sends a dead delivery again with the same ID, URL, payload and signing secret it was queued with. Settled deliveries are purged after control.outbox_retention_days.