Webhooks

Get a signed HTTPS request whenever a session is created.

A webhook sends a signed POST to your HTTPS endpoint whenever a session is created, from any source: the dashboard, the CLI, the API, agents, and handlers.

Create a webhook

1
curl https://api.ellipsis.dev/v1/webhooks -H "Authorization: Bearer $ELLIPSIS_API_TOKEN" -H "Content-Type: application/json" -d '{"url": "https://example.com/hooks/ellipsis", "events": ["session.create"]}'

The response includes a secret that starts with whsec_. Save it: it's shown only once. To change it, delete the webhook and create a new one.

The URL must be public HTTPS on port 443 or 8443. An account can have up to 3 webhooks.

Activate it

  1. Send a test event with POST /v1/webhooks/{webhook_id}/ping. Your endpoint must answer with HTTP 200.
  2. Open Settings > Webhooks and choose Request activation.

Ellipsis turns the webhook on after reviewing the request. Until then it receives only pings.

Events

EventSent when
session.createA session is created
webhook.pingYou call the ping endpoint; always sent

Every delivery has the same envelope. For session.create, data.session is the object GET /v1/sessions/{session_id} returns:

1
{
2
"id": "event_0f8e2c",
3
"type": "session.create",
4
"created_at": "2026-09-22T17:03:11.482913Z",
5
"api_version": "v1",
6
"data": {
7
"session": {
8
"id": "session_4b1d9a",
9
"source": "api",
10
"budget": 5.0,
11
"conversation": {
12
"state": "open"
13
},
14
"turn": {
15
"id": "turn_8c2f1e",
16
"status": "pending"
17
}
18
}
19
}
20
}

This example is shortened; real IDs are longer, and session has every field.

Verify signatures

Each request has 3 headers:

HeaderValue
webhook-idThe event ID; the same on every retry
webhook-timestampUnix seconds when this attempt was sent
webhook-signaturev1, followed by a base64 HMAC-SHA256 signature

To verify, base64-decode your secret without its whsec_ prefix, and sign {webhook-id}.{webhook-timestamp}.{raw body} with it. Use the raw request bytes, not re-serialized JSON:

1
import base64
2
import hashlib
3
import hmac
4
import time
5
6
7
def verify(secret: str, headers: dict[str, str], body: bytes) -> bool:
8
event_id = headers["webhook-id"]
9
timestamp = headers["webhook-timestamp"]
10
if abs(time.time() - int(timestamp)) > 300:
11
return False
12
key = base64.b64decode(secret.removeprefix("whsec_"))
13
signed = f"{event_id}.{timestamp}.".encode() + body
14
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
15
return any(
16
hmac.compare_digest(expected, signature.removeprefix("v1,"))
17
for signature in headers["webhook-signature"].split()
18
)

These examples reject requests older than 5 minutes.

Delivery

  • Any 2xx response within 10 seconds counts as delivered. Pings need exactly 200.
  • A failed delivery is retried after 1 minute and again after 10 minutes, then marked failed.
  • Events can arrive out of order or more than once; use webhook-id to skip duplicates.
  • GET /v1/webhooks/deliveries lists the last 30 days of deliveries, and GET /v1/webhooks/deliveries/{delivery_id} shows one with its payload and attempts.

On this page

Schedule a demo