> ## Documentation Index
> Fetch the complete documentation index at: https://sudomock.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The base URL is https://api.sudomock.com.
> Authenticate every request with the x-api-key header. Keys begin with sm_.
> A render returns the finished image at data.print_files[0].export_path. A request sent with is_async true returns a job_id to poll at GET /api/v1/jobs/{job_id}.
> Prefer the official SDKs over hand-written HTTP calls: npm install sudomock for Node, pip install sudomock for Python.

# Webhooks

> Get a signed HTTPS request the moment a job finishes.

A webhook is an HTTPS request SudoMock sends to your server the moment a job
reaches its final state, so your app reacts to a finished render instead of
asking for it.

## Why use webhooks

Every delivery is a signed JSON body your application can act on:

* Publish a product image the second its render lands
* Alert on a failed render while the order is still open
* Keep your own record of every job the account ran
* Drive a queue of PSD uploads from one route instead of many status calls

<Tip>
  A delivery your server missed can be sent again later, one delivery at a time
  or every failed delivery for an endpoint at once. See [Replay a single
  delivery](/docs/api-reference/webhook-deliveries/replay-a-single-delivery).
</Tip>

## How to receive webhooks

<Steps>
  <Step title="Create an endpoint in your app">
    Add a route that accepts POST requests and answers `200` once the body is
    safely stored.

    ```js app/api/sudomock/route.js theme={"theme":{"light":"github-light","dark":"vesper"}}
    export async function POST(request) {
      const event = await request.json()
      console.log(event)
      return new Response(null, { status: 200 })
    }
    ```

    Any status other than `2xx`, and any timeout, counts as a failed delivery.

    <Tip>
      For local work, put your development server behind a public HTTPS URL with
      a tunnel such as ngrok or the port forwarding built into your editor, then
      register that URL: `https://example123.ngrok.io/api/sudomock`.
    </Tip>
  </Step>

  <Step title="Register the endpoint">
    1. Open [Dashboard, Webhooks](https://sudomock.com/dashboard/webhooks)
    2. Add your public HTTPS URL
    3. Pick the events you want, or leave the selection empty for all of them
    4. Copy the signing secret before you close the dialog

    The secret is prefixed `whsec_` and is shown in full only when it is
    created and when it is rotated. Every later read masks it as
    `whsec_****<last4>`.

    <Info>
      Endpoints can also be managed from your own backend. See [Create a new
      webhook
      endpoint](/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint).
    </Info>
  </Step>

  <Step title="Send a test event">
    Send a test delivery from the panel, or call [Send a test
    event](/docs/api-reference/webhook-endpoints/send-a-test-event). It travels the
    same signed path as a real one, so verification that passes here passes in
    production.

    A finished render arrives in this shape:

    ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
    {
      "event": "render.succeeded",
      "job_id": "c315f78f-d2c7-4541-b240-a9372842de94",
      "kind": "render",
      "status": "succeeded",
      "result_url": "https://cdn.sudomock.com/renders/c315f78f.png",
      "error": null,
      "created_at": "2026-06-21T10:00:00Z"
    }
    ```

    <Info>
      Every event is listed in [Events](#events), and every field of the body in
      [The payload](#the-payload).
    </Info>
  </Step>

  <Step title="Verify the signature, then handle the event">
    Check the signature before you act on the body, then branch on `event`.

    ```js app/api/sudomock/route.js theme={"theme":{"light":"github-light","dark":"vesper"}}
    export async function POST(request) {
      const raw = await request.text()

      if (!verifySudoMockWebhook(request.headers, raw, secret)) {
        return new Response("invalid signature", { status: 400 })
      }

      const event = JSON.parse(raw)
      if (event.event === "render.succeeded") {
        await publish(event.job_id, event.result_url)
      }

      return new Response(null, { status: 200 })
    }
    ```

    [Verifying signatures](/docs/webhooks/verifying-signatures) carries the full
    verification function in Node.js and Python.
  </Step>

  <Step title="Move the endpoint to production">
    Deploy the handler, then register the production URL the same way. Each
    endpoint has its own secret, so the development one can stay registered
    beside it.
  </Step>
</Steps>

## Events

| Event                           | When it fires                                                 |
| ------------------------------- | ------------------------------------------------------------- |
| `render.succeeded`              | An image render job finished successfully.                    |
| `render.failed`                 | An image render job failed.                                   |
| `upload.succeeded`              | A PSD upload finished parsing into a mockup.                  |
| `video.succeeded`               | A video job finished successfully.                            |
| `video.failed`                  | A video job failed.                                           |
| `photo_mockup.ready`            | A product photo became a reusable photo mockup.               |
| `photo_mockup.rejected`         | A product photo was not suitable for a photo mockup.          |
| `photo_mockup.failed`           | A photo mockup creation job failed unexpectedly.              |
| `photo_mockup_render.succeeded` | An asynchronous photo mockup render finished successfully.    |
| `photo_mockup_render.failed`    | An asynchronous photo mockup render failed.                   |
| `webhook.test`                  | Fired by the test action in the dashboard or by `POST /test`. |

Leave `event_types` empty to subscribe to every event, including ones added
later. A failed upload is delivered as `render.failed`, so subscribe to
`render.failed` if you ingest PSDs.

## The payload

Every delivery opens with the same envelope. A render, an upload, a video and
a photo mockup render carry the fields below. The three `photo_mockup.*`
creation events carry the mockup's own fields instead, and [Photo
mockups](/docs/photo-mockups/overview) covers them.

<ParamField body="event" type="string">
  The event that fired, spelled exactly as in the table above.
</ParamField>

<ParamField body="job_id" type="string">
  The job this event belongs to. It is the id you pass to `GET
      /api/v1/jobs/{job_id}`, and half of the idempotency key.
</ParamField>

<ParamField body="kind" type="string">
  The job family behind the event, for example `render`, `upload` or `video`.
  An endpoint subscribed to everything can filter on it without parsing the
  event name.
</ParamField>

<ParamField body="status" type="succeeded | failed | ready | rejected">
  The final state the job reached. It matches the second half of the event
  name.
</ParamField>

<ParamField body="result_url" type="string | null">
  The finished file for a render or a video, and the new `mockup_uuid` for
  `upload.succeeded`. It is `null` on a failure, and it can be `null` on a
  delivery that was replayed.
</ParamField>

<ParamField body="error" type="object | null">
  `null` on success. On a failure it carries the same structured error the API
  returns, with `error_code` and `message`. See [Errors](/docs/errors).
</ParamField>

<ParamField body="created_at" type="string">
  ISO 8601 timestamp of the moment the event was created.
</ParamField>

The body arrives as a `POST` with `Content-Type: application/json`, and two
more headers travel with it, `X-SudoMock-Signature` and
`X-SudoMock-Timestamp`. [Verifying
signatures](/docs/webhooks/verifying-signatures) shows what to compute from them.

## FAQ

<AccordionGroup>
  <Accordion title="What happens when a delivery fails?">
    Failed deliveries are retried automatically. Every attempt is logged with
    the HTTP status your server returned, the attempt count and the last error,
    and you can send one again yourself once the server is back.

    From the panel:

    1. Open [Dashboard, Webhooks](https://sudomock.com/dashboard/webhooks)
    2. Open the endpoint
    3. Open the delivery you want to send again
    4. Replay it

    From your own backend, call [Replay a single
    delivery](/docs/api-reference/webhook-deliveries/replay-a-single-delivery), or
    [Replay all failed
    deliveries](/docs/api-reference/webhook-deliveries/replay-all-failed-deliveries)
    for the whole endpoint.
  </Accordion>

  <Accordion title="How do I keep from acting on one event twice?">
    Treat `job_id` plus the event name as the delivery's idempotency key,
    persist it before applying side effects, and return a `2xx` response only
    after processing succeeds. A replayed delivery can arrive with `result_url`
    set to `null`; fetch the result by polling `GET /api/v1/jobs/{job_id}` when
    that happens.
  </Accordion>

  <Accordion title="Do webhooks replace polling?">
    No. `GET /api/v1/jobs/{job_id}` stays the source of truth for a job, and a
    webhook is the notification that saves you from asking on a timer.
  </Accordion>

  <Accordion title="How do I page through deliveries and events?">
    The delivery and event feeds page with an opaque cursor. Make the first
    request without `cursor`. When another page exists, the response carries
    `X-Webhook-Next-Cursor`, and you send that exact value as the next
    request's `cursor`. When the header is absent, the list is complete.
    [Pagination](/docs/api-reference/pagination) covers the other list endpoints.
  </Accordion>

  <Accordion title="Which endpoints receive the earlier photo mockup names?">
    Endpoints created before the `photo_mockup` names were introduced are
    pinned to the earlier spelling of the same five events:
    `2d_mockup.ready`, `2d_mockup.rejected`, `2d_mockup.failed`,
    `2d_render.succeeded` and `2d_render.failed`, with `kind` spelled
    `2d_create` or `2d_render`. They keep receiving them unchanged. The
    endpoint's `event_naming` field reads `legacy` or `current` and says which
    spelling it receives; [Create a new webhook
    endpoint](/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint)
    covers how a new endpoint is pinned. Either spelling is accepted in
    `event_types`. Move an existing endpoint with `PATCH { "event_naming":
            "current" }` once your handler reads the new names.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Verifying signatures" icon="shield-check" href="/docs/webhooks/verifying-signatures">
    Node.js and Python you can paste into your handler.
  </Card>

  <Card title="Webhook endpoints" icon="code" href="/docs/api-reference/webhook-endpoints/create-a-new-webhook-endpoint">
    Create, update, rotate and test an endpoint from your backend.
  </Card>

  <Card title="Webhook deliveries" icon="list" href="/docs/api-reference/webhook-deliveries/retrieve-a-list-of-deliveries">
    Read every attempt, and replay the ones that failed.
  </Card>

  <Card title="Watch deliveries from the panel" icon="gauge" href="/docs/dashboard/webhooks">
    Success rate, delivery log and the account-wide event feed.
  </Card>
</CardGroup>
