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.

HTTP
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

GET/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

cURL
curl 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).

json
{
  "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

GET/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

cURL
curl https://exe.krallo.com/api/v1/webhooks/deliveries/01J8ZC4R7M0F3A9Q2W5X8Y1B6D \
  -H "Authorization: Bearer krallo_live_<your-key>"

Response

200 OK — data is the delivery object.

json
{
  "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

POST/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:

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

cURL
curl -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.

json
{
  "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