Files and webhooks

Upload files, publish assets, and subscribe to events with 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:

Terminal
curl -X POST "$A13N_URL/api/v1/uploads" \
  -H "Authorization: Bearer $A13N_API_KEY" -H "Idempotency-Key: report-2026-09" -F [email protected]

The response gives the upload_id, filename, content type, size and SHA-256 digest. Retrying with the same caller, workspace, key, bytes, filename and content type returns the same upload. Changing any upload content or metadata under that 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, named content. You create assets from uploads, agents can publish them, and you attach them to messages.

  • Create from an upload with POST /api/v1/assets and {"upload_id": ..., "name": ...}. The first creation returns 201; repeating it with the same upload and name returns the same asset with 200. An upload becomes at most one asset.
  • Read with GET …/assets, GET …/assets/{asset_id}, and GET …/assets/{asset_id}/content, which returns the bytes as an attachment.
  • Retire with DELETE …/assets/{asset_id} and its If-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.

Terminal
curl -X POST "$A13N_URL/api/v1/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_..."}}'
  • kinds selects events: run.accepted, run.running, run.waiting, run.completed, run.failed, run.cancelled, and for attempts run_attempt.leased, run_attempt.running, run_attempt.succeeded, run_attempt.yielded, run_attempt.failed, run_attempt.cancelled.
  • filter optionally narrows events to an agent_id, session_id or thread_id.
  • signing_secret (16–256 characters) is optional; without it the Service generates one. The secret is returned only in the creation response.
  • PATCH changes the name, URL, kinds, filter, enabled or signing_secret; DELETE removes the subscription. Both take If-Match. Changes affect only deliveries queued afterwards.
  • A workspace has at most control.subscriptions subscriptions (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:

JSON
{
  "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:

HeaderValue
X-A13n-Delivery-IdThe delivery ID, also the payload id. Use it to discard duplicates.
X-A13n-Webhook-TimestampUnix seconds when the request was signed.
X-A13n-Webhook-Signaturev1= 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 stale timestamps. This example allows five minutes of clock difference; choose a window for your receiver and keep its clock synchronized:

Python
import hashlib
import hmac
import time

def verify(secret: str, headers, body: bytes, *, max_age_seconds: int = 300) -> bool:
    try:
        timestamp = headers["X-A13n-Webhook-Timestamp"]
        delivery_id = headers["X-A13n-Delivery-Id"]
        received = headers["X-A13n-Webhook-Signature"]
        if abs(time.time() - int(timestamp)) > max_age_seconds:
            return False
    except (KeyError, ValueError):
        return False
    signed = f"{timestamp}.{delivery_id}.".encode() + body
    expected = "v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)

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 (up to 2, 4, 8, … seconds with 20% jitter, capped at one hour) until it has used the webhook outbox policy's max_attempts attempts (outbox.by_kind.webhook, else outbox.defaults; 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 the webhook retention period: by default one day after delivery, and fourteen days after a delivery became dead.

On this page