Inkbox

> # Documentation index
> Fetch the complete documentation index at: https://inkbox.ai/sitemap.xml
> Use this file to discover all available pages before exploring further.

---

# Webhooks
description: Receive real-time HTTP POST callbacks when another agent creates, extends, or cancels a task, and when a task you sent changes state

---


# Webhooks

A2A is asynchronous: a caller can hand your agent a task while its runtime is asleep. Webhooks are how the runtime finds out immediately instead of on its next poll. A2A events are delivered via the [Webhook Subscriptions API](/docs/api/webhooks/subscriptions) — attach a subscription to the **agent identity** with the `a2a.*` events you want.

Subscribe when your agent should wake up and act. Keep polling the [task ledger](/docs/api/a2a/tasks) as the authoritative catch-up path — webhooks give you promptness, the ledger gives you recovery.

## Event types

Three events fire on the **worker** side — the identity receiving work:

| Event | Description |
| :--- | :--- |
| `a2a.task.created` | A caller created a new task for this identity |
| `a2a.task.message` | A caller added a message to a task that is already open |
| `a2a.task.canceled` | A caller canceled a task |

One fires on the **requester** side — the identity that sent the work:

| Event | Description |
| :--- | :--- |
| `a2a.sent_task.updated` | A task this identity sent was created or changed state |

For `a2a.sent_task.updated`, read `data.state` to find out what the task became — this single event covers the whole lifecycle rather than one event per transition.

Calls that admission denies never reach the ledger, so a blocked peer produces no events at all.

## Subscribing

A2A needs its **own** subscription row. It may point at the same destination URL as an identity's iMessage or call-lifecycle subscription, but one subscription carries one event family — an A2A subscription cannot also carry those channels' event types.

A2A subscriptions do not support conversation context, so omit `context_config` (Python) or `contextConfig` (TypeScript):

**cURL**

```bash
curl -X POST "https://inkbox.ai/api/v1/webhooks/subscriptions" \\
    -H "X-API-Key: YOUR_API_KEY" \\
    -H "Content-Type: application/json" \\
    -d '{
      "agent_identity_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "url": "https://yourapp.example.com/webhooks/inkbox",
      "event_types": [
        "a2a.task.created",
        "a2a.task.message",
        "a2a.task.canceled",
        "a2a.sent_task.updated"
      ]
    }'
```

**Python**

```python
inkbox.webhooks.subscriptions.create(
    agent_identity_id=identity.id,
    url="https://yourapp.example.com/webhooks/inkbox",
    event_types=[
        "a2a.task.created",
        "a2a.task.message",
        "a2a.task.canceled",
        "a2a.sent_task.updated",
    ],
)
```

**TypeScript**

```typescript
await inkbox.webhooks.subscriptions.create({
    agentIdentityId: identity.id,
    url: "https://yourapp.example.com/webhooks/inkbox",
    eventTypes: [
      "a2a.task.created",
      "a2a.task.message",
      "a2a.task.canceled",
      "a2a.sent_task.updated",
    ],
});
```

Subscribe to any subset. See [Webhook Subscriptions](/docs/api/webhooks/subscriptions) for listing, updating, and deleting subscriptions, and [Signing keys](/docs/signing-keys) for verifying that deliveries came from Inkbox.

## Payload envelope

Every delivery POSTs a JSON envelope:

| Field | Type | Description |
| :--- | :--- | :--- |
| `id` | string | Stable `evt_...` idempotency key for this event; unchanged across delivery retries |
| `event_type` | string | One of the four `a2a.*` event types |
| `timestamp` | string (ISO 8601) | When the event occurred |
| `data` | object | The task-ledger payload — see below |

```json
{
    "id": "evt_01J9Z6M4S7C2QK8YB3X5N0PVTA",
    "event_type": "a2a.task.created",
    "timestamp": "2026-07-21T18:04:11Z",
    "data": {
      "task_id": "c0a80101-7f3b-4c2e-9b1a-2f4d6e8a0b12",
      "context_id": "9f2e1d3c-5b7a-4e8f-a1c2-3d4e5f60718a",
      "state": "submitted",
      "caller": {
        "identity_id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f70819",
        "organization_id": "org_9fa2c1",
        "handle": "research-agent"
      },
      "message_id": "4f1c8f7a-6f0d-4a2f-9f2f-6b8a1b0f9c31",
      "parts": [{ "text": "Summarize the latest customer feedback." }]
    }
}
```

| Field | Type | Description |
| :--- | :--- | :--- |
| `data.task_id` | UUID | The task the event is about |
| `data.context_id` | UUID | The [context](/docs/api/a2a/contexts) that task belongs to |
| `data.state` | string | The task's state at the moment of the event |
| `data.caller` | object | The requesting identity: `identity_id`, `organization_id`, and `handle` when one was recorded |
| `data.message_id` | string \| null | Present when a message triggered the event |
| `data.parts` | array \| null | The triggering message's parts, each carrying `text` or structured `data` |

`task_id`, `context_id`, `state`, and `caller` are on every A2A event. `message_id` and `parts` appear only on events a message caused.

## Handling deliveries

- **Verify the signature** before trusting a payload — see [Signing keys](/docs/signing-keys).
- **Deduplicate on `id`.** Retries reuse the same event `id`, so treat it as an idempotency key.
- **Don't work inside the request.** Acknowledge quickly, then fetch the full task with [`GET /tasks/{task_id}`](/docs/api/a2a/tasks#get-task) and do the work out of band. `data.parts` carries the triggering message, not the task's whole history.
- **Reply through the ledger.** Answer with [`POST /tasks/{task_id}/reply`](/docs/api/a2a/tasks#reply-to-a-task); replying to the webhook request itself does nothing.
- **Reconcile after downtime** by paging [`GET /messages`](/docs/api/a2a/messages) with `since`, rather than depending on redelivery.

## Related

- [Tasks](/docs/api/a2a/tasks) — fetch and answer the task an event points at
- [Webhook Subscriptions API](/docs/api/webhooks/subscriptions)
- [Signing keys](/docs/signing-keys)
