---
title: Webhooks
description: Receive signed terminal job events from Sume.
---

Use webhooks when your server should be notified when a job reaches a terminal
state.

**This page is about generation jobs.** Sume has two webhook surfaces:

| You called | You get | Documented at |
|---|---|---|
| `POST /v1/models/...` or a model endpoint like `/v1/avatar-1.0/generate` | `job.completed` / `job.failed` / `job.canceled` | This page |
| An Action, Format, or Agent Completion run endpoint | `action.run.terminal` / `format.run.terminal` / `agent.run.terminal` | [Run webhooks](/agents/run-webhooks) |

The event sets do not overlap and the payloads differ — a run webhook carries the
full run receipt, not a job result. The signature scheme is identical, so one
verifier covers both.

## Submit with a webhook URL

Send `mode: "webhook"` with `webhook_url`.

<!-- api-call-example:avatar-generate-webhook -->

Webhook URLs must be public HTTPS URLs. Localhost, private-network, and
non-HTTPS URLs are rejected.

Webhook is one of four communication modes. See
[Communication modes](/workflows/jobs-and-results) for how it compares to
`async`, `sync`, and `subscribe`, and for the polling fallback you should keep
in place alongside it.

## Events

Sume sends **terminal job events only**. There are no progress or partial
deliveries:

| Event | When it is sent |
|---|---|
| `job.completed` | The job completed and a public result is available. |
| `job.failed` | The job failed with a public error. |
| `job.canceled` | The job reached canceled state. |

## Payload

```json
{
  "event": "job.completed",
  "request_id": "job_...",
  "job_id": "job_...",
  "status": "OK",
  "payload": {
    "artifacts": [
      {
        "id": "artifact_...",
        "url": "https://media.sume.com/artifacts/...",
        "type": "image",
        "content_type": "image/png"
      }
    ]
  }
}
```

Failed and canceled webhooks use `status: "ERROR"` and include an `error`
object.

## Signature headers

When webhook signing is configured, Sume signs the raw JSON body with HMAC SHA
256 over:

```text
<timestamp>.<raw_body>
```

Headers:

```text
x-sume-webhook-timestamp: 1780000000
x-sume-webhook-signature: sume-v1=<hex_signature>
```

Reject callbacks when the timestamp is outside your replay tolerance window.
Five minutes is a reasonable default.

Read your signing secret on the **Webhooks** tab of the dashboard
(`/dashboard/webhooks` — Reveal, then copy), or from
`GET /v1/webhooks/signing-secret` with any API key carrying `account:read`. It
is derived for your workspace, so it is yours rather than a shared platform
value. Store it as `SUME_COM_WEBHOOK_SIGNING_SECRET`, the same name the delivery
worker signs with. Job webhooks and [run webhooks](/agents/run-webhooks) share
that one secret, so a single verifier covers both.

Every delivery also carries `x-sume-webhook-secret-fingerprint`, and
`webhook_delivery.signing_secret_fingerprint` on the receipt repeats it. If a
signature does not verify, compare that fingerprint with the one shown beside
the secret in the dashboard — neither side ever has to send the secret itself.

## Verify in TypeScript

```ts
import crypto from "node:crypto";

export function verifySumeWebhook({
  rawBody,
  timestamp,
  signatureHeader,
  secret,
  toleranceSeconds = 300,
}: {
  rawBody: string;
  timestamp: string;
  signatureHeader: string;
  secret: string;
  toleranceSeconds?: number;
}) {
  const ts = Number(timestamp);
  if (!Number.isFinite(ts)) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > toleranceSeconds) {
    return false;
  }

  const digest = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");
  const expected = `sume-v1=${digest}`;
  const actualBuffer = Buffer.from(signatureHeader);
  const expectedBuffer = Buffer.from(expected);
  if (actualBuffer.length !== expectedBuffer.length) return false;

  return crypto.timingSafeEqual(actualBuffer, expectedBuffer);
}
```

## Delivery behavior

Return any `2xx` response after durably storing the event. Network errors and
non-2xx responses are retried until attempts are exhausted. Use `job_id` as the
idempotency key on your side.

| | |
|---|---|
| Retries | Up to **10** attempts total. |
| Spacing | A fixed delay between attempts (30s by default), not exponential backoff. |
| Timeout | 10s per attempt. A slow endpoint burns the budget and gets retried. |

Ten refused attempts leave you with a failed *delivery* and a job that still
reached its real terminal state. Delivery is an optimization, never the only
recovery path — keep `status_url` polling available for the events that never
arrive.

## Send test and Redeliver

Two different actions. Do not substitute one for the other.

**Send test** is one control on `/dashboard/webhooks` (or
`POST /v1/webhooks/test-deliveries` with `account:write`). It POSTs a dummy
signed `webhook.test` payload to a URL *you type*. It never replays a real
job. The dummy body has no `job_id` / `arun_` and is not appended to
Requests.

```json
{
  "event": "webhook.test",
  "request_id": "req_wh_test_…",
  "payload": {
    "ok": true,
    "message": "Sume webhook test. Not a job or Format run."
  }
}
```

**Redeliver** is per call. On each delivery row, or
`POST /v1/jobs/{job_id}/webhook/redeliver` (`jobs:write`), Sume re-POSTs that
job's real terminal event (`job.completed` / `job.failed` / `job.canceled`)
with a **fresh** timestamp and signature. This still works after automatic
attempts are exhausted — it does not consume one of the automatic 10.

Receivers must treat `job_id` as the idempotency key. Redeliver does not
change the destination URL; a new URL is a new job.

Format run redeliver is documented on [Run webhooks](/agents/run-webhooks).

[Run webhooks](/agents/run-webhooks) use the same 10-attempt cap on a different
schedule; that page is authoritative for `*.run.terminal` deliveries.

Webhook delivery status, including the attempt count, is visible on the job
object and in job events when available.

## Next

- [Run webhooks](/agents/run-webhooks) — the same signature scheme for Action,
  Format, and Agent Completion runs
