API reference
Webhook deliveries API
Inspect and replay your webhook deliveries.
A delivery is one attempt-tracked row per (event × endpoint) pair — the durable state machine the platform drives to push an event to one of your webhook endpoints. This API lets you list your recent deliveries, read a single delivery's current state, and manually re-queue one for another attempt. Every operation is bearer-authenticated, scope-gated, and strictly scoped to deliveries you own.
Authentication and scopes
All three endpoints are part of the public /api/v1 surface. Authenticate with a developer API
key as a bearer token — never cookies or CSRF. See Authentication
for how to mint and send a key.
Authorization: Bearer krallo_live_<your-key>
| Operation | Required scope |
|---|---|
| List deliveries · Retrieve a delivery | webhooks:read |
| Replay a delivery | webhooks:write |
A key holding the staff-only * wildcard satisfies either check. A key missing the required
scope gets 403 insufficient_scope with the needed scope echoed in
error.required_scope. See Scopes for the full vocabulary.
All three calls are metered like any other billable request: each counts against your daily free quota and, once the quota is exhausted, debits your credit balance. A call that fails after metering (a 404, 409, or 500) is automatically refunded, so a rejected replay never keeps your credits. See Billing.
The delivery object
Every endpoint returns the same masked delivery shape. Only the fields below are exposed — internal columns such as the sending lease are never serialized.
| Field | Type | Nullable | Description |
|---|---|---|---|
id |
string | No | The delivery's unique id (a ULID). Use this in the retrieve and replay paths. |
endpoint_id |
string | No | The webhook endpoint this delivery targets. |
event_id |
string | No | The source event's id (a ULID). Matches the id in the delivered webhook envelope and the X-Krallo-Event-Id header — stable across retries and replays, so it is your idempotency key. |
event_type |
string | No | The event name, e.g. imagine.completed. See the event catalog. |
status |
string | No | Lifecycle state: pending, sending, delivered, or dead. See the lifecycle. |
attempt_count |
integer | No | How many send attempts have been made so far. Reset to 0 by a replay. |
next_attempt_at |
integer | Yes | Unix epoch milliseconds of the next scheduled attempt while pending. null once the delivery is delivered or dead. |
last_response_code |
integer | Yes | HTTP status code from your endpoint on the most recent attempt. null if no attempt has produced a response yet (e.g. a connection error, timeout, or SSRF block). |
last_error |
string | Yes | Short error reason from the most recent failed attempt (clipped to 512 chars), or a terminal reason for a dead-lettered delivery such as endpoint_inactive or guild_admin_revoked. null on success. |
created_at |
integer | No | Unix epoch milliseconds when the delivery row was created (when the event fanned out to this endpoint). |
updated_at |
integer | No | Unix epoch milliseconds of the last state change. |
Delivery timestamps (created_at, updated_at, next_attempt_at) are epoch milliseconds. This differs from the created_at inside a delivered webhook event envelope, which is epoch seconds. Don't mix the two units.
Delivery status lifecycle
A delivery moves through these states as the delivery worker sweeps it:
| Status | Meaning |
|---|---|
| pending | Queued and awaiting its next attempt at next_attempt_at. A newly created or freshly replayed delivery starts here. |
| sending | Claimed by a worker and being delivered right now, under a short lease. If the worker crashes, the lease lapses and the row is re-claimed automatically. A delivery cannot be replayed while in this state. |
| delivered | Your endpoint returned a success. Terminal; next_attempt_at is null and last_response_code holds the accepting status. |
| dead | The delivery exhausted its retry budget or hit a non-retryable terminal condition (endpoint deleted, disabled, secret undecryptable, or guild authorization revoked). Terminal until you replay it. last_error carries the reason. |
Failed attempts are retried with capped exponential backoff up to a maximum attempt count (default 12), each
rescheduling next_attempt_at further out (base 5s, capped at 10 minutes). After the budget is
spent the delivery is marked dead. Repeated endpoint failures can also auto-disable the endpoint.
Delivery is at-least-once: dedupe on event_id in your consumer.
List deliveries
/api/v1/webhooks/deliveries
Returns your recent deliveries across all of your endpoints, newest first (ordered by created_at
descending, then id descending). Requires webhooks:read.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit |
integer | No | Maximum rows to return. Clamped to the range 1–100; defaults to 25. A missing, blank, or non-numeric value falls back to the default. |
The list is a bounded most-recent window, not a cursor-paginated feed — there is no offset or page token. For a durable record of every delivery, consume and persist them from your endpoint. This API is for inspection and replay.
Request
cURLcurl https://exe.krallo.com/api/v1/webhooks/deliveries?limit=2 \
-H "Authorization: Bearer krallo_live_<your-key>"
Response
200 OK — data is an array of delivery objects (possibly empty).
{
"data": [
{
"id": "01J8ZC4R7M0F3A9Q2W5X8Y1B6D",
"endpoint_id": "01J8ZC1A2B3C4D5E6F7G8H9J0K",
"event_id": "01J8Z9K3M4P5Q6R7S8T9V0W1X2",
"event_type": "imagine.completed",
"status": "delivered",
"attempt_count": 1,
"next_attempt_at": null,
"last_response_code": 200,
"last_error": null,
"created_at": 1752566400123,
"updated_at": 1752566401987
},
{
"id": "01J8ZB0H7K2M4N6P8R0T2V4X6Z",
"endpoint_id": "01J8ZC1A2B3C4D5E6F7G8H9J0K",
"event_type": "imagine.completed",
"event_id": "01J8Z8Q1W2E3R4T5Y6U7I8O9P0",
"status": "dead",
"attempt_count": 12,
"next_attempt_at": null,
"last_response_code": 500,
"last_error": "endpoint returned 500",
"created_at": 1752560000000,
"updated_at": 1752561800000
}
],
"meta": { "request_id": "e8c0a2d4-5f7b-4e9d-a1c3-6b8d0f2a4c6e" }
}
Retrieve a delivery
/api/v1/webhooks/deliveries/{id}Returns the current state of a single delivery you own. Requires webhooks:read.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | The delivery id (a ULID) from a list response or a X-Krallo-Webhook-Id header. |
Request
cURLcurl https://exe.krallo.com/api/v1/webhooks/deliveries/01J8ZC4R7M0F3A9Q2W5X8Y1B6D \
-H "Authorization: Bearer krallo_live_<your-key>"
Response
200 OK — data is the delivery object.
{
"data": {
"id": "01J8ZC4R7M0F3A9Q2W5X8Y1B6D",
"endpoint_id": "01J8ZC1A2B3C4D5E6F7G8H9J0K",
"event_id": "01J8Z9K3M4P5Q6R7S8T9V0W1X2",
"event_type": "imagine.completed",
"status": "delivered",
"attempt_count": 1,
"next_attempt_at": null,
"last_response_code": 200,
"last_error": null,
"created_at": 1752566400123,
"updated_at": 1752566401987
},
"meta": { "request_id": "f1d3b5a7-9c8e-4f0a-b2d4-7c9e1a3b5d7f" }
}
A delivery that does not exist — or that belongs to another developer — returns
404 not_found. See Ownership and access.
Replay a delivery
/api/v1/webhooks/deliveries/{id}/replay
Re-queues a delivery for another attempt using the original event payload. Requires
webhooks:write. The request body is ignored.
Replaying resets the delivery in place:
status→pendingattempt_count→0next_attempt_at→ now (it will be swept on the next worker pass)last_response_codeandlast_error→null
A replay does not create a new delivery id or event id — the same row is redriven, so your consumer receives
the same event_id again. Attempt-log rows from the original round are preserved; the replay
appends a fresh round.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | The delivery id (a ULID) to replay. Any status except sending is replayable, including an already delivered one. |
Request
cURLcurl -X POST \
https://exe.krallo.com/api/v1/webhooks/deliveries/01J8ZB0H7K2M4N6P8R0T2V4X6Z/replay \
-H "Authorization: Bearer krallo_live_<your-key>"
Response
202 Accepted — the delivery has been re-queued; data is the delivery in its reset state.
{
"data": {
"id": "01J8ZB0H7K2M4N6P8R0T2V4X6Z",
"endpoint_id": "01J8ZC1A2B3C4D5E6F7G8H9J0K",
"event_id": "01J8Z8Q1W2E3R4T5Y6U7I8O9P0",
"event_type": "imagine.completed",
"status": "pending",
"attempt_count": 0,
"next_attempt_at": 1752570000000,
"last_response_code": null,
"last_error": null,
"created_at": 1752560000000,
"updated_at": 1752570000000
},
"meta": { "request_id": "a6c8e0d2-4b5f-4a7c-9e1b-3d5f7a9c1e3b" }
}
A delivery that is currently sending cannot be replayed — redriving it would race the worker's in-flight completion write. The call returns 409 delivery_in_flight. Wait for it to settle to delivered or dead, then replay.
Ownership and access
Every operation is IDOR-scoped: the service joins each delivery through its endpoint and app
to the owning account and filters on your identity (including your
linked identities). A delivery id that does not resolve to one of
your apps is indistinguishable from one that does not exist — both return 404 not_found. There is
no way to read or replay another developer's delivery, and the 404 leaks nothing about whether the id is real.
Errors
Errors use the standard /api/v1 envelope: { "error": { "code", "message", … }, "meta": {
"request_id" } }. See Errors for the full model.
| Status | Code | When |
|---|---|---|
| 401 | unauthorized / invalid_token / token_revoked / token_expired |
Missing, malformed, unknown, revoked, or expired bearer token. |
| 402 | insufficient_credits |
Daily free quota is exhausted and your credit balance can't cover the call. error.required_units and error.balance are included. See Billing. |
| 403 | insufficient_scope |
Key lacks webhooks:read (list/get) or webhooks:write (replay). The required scope is in error.required_scope. |
| 403 | app_disabled / owner_suspended |
The owning app is deactivated, or the account (across linked identities) is suspended. |
| 404 | not_found |
Retrieve or replay of an unknown delivery id, or one owned by another developer (IDOR-scoped). |
| 405 | method_not_allowed |
Wrong method for the path — e.g. POST to the list/get path, or GET to /replay. |
| 409 | delivery_in_flight |
Replay attempted while the delivery is sending. |
| 429 | rate_limited |
Per-key rate limit exceeded. Carries Retry-After and X-RateLimit-* headers. See Rate limits. |
| 500 | internal_error |
Unexpected store or platform failure. Any metering for the call is refunded. Safe to retry. |
If the developer API is turned off platform-wide, every path — including these — returns a bare 404 not_found rather than revealing the surface. The 429 rate-limit response is emitted by the shared limiter as { "error": "rate_limited", "hint": …, "retryAfterSeconds": … }, which is flatter than the standard envelope.
Next steps
- Webhooks overview — endpoints, subscriptions, and delivery semantics.
- Event catalog — the events that produce deliveries, and their payloads.
- Signature verification — verify a delivery is genuinely from Krallo.
- Errors and Rate limits — the response envelope and limit headers.