SudoMock

Webhooks

Receive a signed HTTPS request the moment a render, upload or video job finishes, with no polling loop required.

Overview

Register one or more endpoints in your dashboard and SudoMock will POST a signed JSON payload to them when your jobs complete. Webhooks are free on every plan. Polling GET /api/v1/jobs/{uuid} remains the source of truth, and webhooks are a convenience layer on top.

Manage endpoints in the dashboard

Add, edit and rotate webhook endpoints from Dashboard → Webhooks. Each endpoint has its own signing secret (prefixed whsec_), shown once when you create or rotate it.

Events

EventWhen it fires
render.succeededAn image render job finished successfully.
render.failedAn image render job failed.
upload.succeededA PSD upload finished parsing into a mockup.
video.succeededA video job finished successfully.
video.failedA video job failed.
webhook.testFired by the "Send test" action in the dashboard.

Choose All events (an endpoint with no explicit event filter) to also receive events added in the future. The video.succeeded and video.failed events cover video jobs; render.succeeded, render.failed and upload.succeeded cover the async render and upload jobs you track with GET /api/v1/jobs/{job_id}. A failed upload is delivered as render.failed (there is no upload.failed event), so subscribe to render.failed if you ingest PSDs.

Payload & headers

Headers

http
1X-SudoMock-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
2X-SudoMock-Timestamp: 1718900000
3Content-Type: application/json

Body

json
1{
2 "event": "render.succeeded",
3 "job_id": "c315f78f-d2c7-4541-b240-a9372842de94",
4 "kind": "render",
5 "status": "succeeded",
6 "result_url": "https://cdn.sudomock.com/renders/c315f78f.png",
7 "error": null,
8 "created_at": "2026-06-21T10:00:00Z"
9}

Verifying signatures

Every delivery carries X-SudoMock-Signature and X-SudoMock-Timestamp. The signature is HMAC-SHA256(secret, "{timestamp}.{rawBody}"), hex-encoded. Always compute the HMAC over the raw request body (not a re-serialized object), compare it in constant time, and reject timestamps outside a small window to prevent replays.

Verify webhook signature
javascript
1import crypto from 'crypto'
2
3// Use the RAW request body (e.g. express.raw), not parsed JSON.
4function verifySudoMockWebhook(req, secret) {
5 const signature = req.header('X-SudoMock-Signature')
6 const timestamp = req.header('X-SudoMock-Timestamp')
7 const rawBody = req.body.toString('utf8')
8
9 // Reject replays older than 5 minutes.
10 const age = Math.floor(Date.now() / 1000) - Number(timestamp)
11 if (!timestamp || Math.abs(age) > 300) return false
12
13 const expected = crypto
14 .createHmac('sha256', secret)
15 .update(`${timestamp}.${rawBody}`)
16 .digest('hex')
17
18 return crypto.timingSafeEqual(
19 Buffer.from(signature || '', 'hex'),
20 Buffer.from(expected, 'hex')
21 )
22}

Always verify before trusting a payload

A request that fails verification must be rejected. Never act on a webhook body whose signature you have not validated against your endpoint's secret.

Managing endpoints via the API

Everything you can do from the dashboard you can also do programmatically against the /api/v1/webhook-endpoints resource. These routes accept either a Supabase Authorization: Bearer <JWT> (used by the dashboard) or an x-api-key, so you can create, rotate, test and inspect endpoints from your own backend.

Method & pathWhat it does
POST /api/v1/webhook-endpointsCreate an endpoint. Body: { url (https), description?, event_types[] }. Returns 201 with the full whsec_ secret — shown once.
GET /api/v1/webhook-endpointsList your endpoints (secrets masked as whsec_****<last4>).
GET /api/v1/webhook-endpoints/{id}Get one endpoint.
PATCH /api/v1/webhook-endpoints/{id}Update url, description, event_types, or enabled.
DELETE /api/v1/webhook-endpoints/{id}Hard-delete the endpoint (its deliveries cascade). Returns 204.
POST /api/v1/webhook-endpoints/{id}/rotate-secretIssue a new signing secret. Returns the full whsec_ secret once.
POST /api/v1/webhook-endpoints/{id}/testSend a synthetic webhook.test through the real signed path. Returns 202 { status, event_type, job_id }.
GET /api/v1/webhook-endpoints/{id}/deliveriesList delivery attempts for one endpoint. Query: status?, event_type?, limit (1-200, default 50).
GET /api/v1/webhook-endpoints/eventsRecent deliveries across ALL your endpoints (the dashboard Events feed). Query: status?, event_type?, limit (1-200, default 100).
POST /api/v1/webhook-endpoints/{id}/deliveries/{delivery_id}/replayReplay a single delivery with the same idempotency key. Returns 202 { status, delivery_id }.
POST /api/v1/webhook-endpoints/{id}/deliveries/replay-failedReplay every failed/dead delivery for the endpoint. Returns 202 { status, count }.

Endpoint object

event_types is the list of event names this endpoint subscribes to; an empty array means all events (including ones added later). The secret is only returned in full on create and rotate-secret; every other read masks it.

json
1{
2 "id": "we_8f3a...",
3 "url": "https://your-domain.com/hooks/sudomock",
4 "secret": "whsec_****a1b2",
5 "description": "Production listener",
6 "event_types": ["render.succeeded", "video.succeeded"],
7 "enabled": true,
8 "created_at": "2026-06-21T10:00:00Z",
9 "updated_at": null
10}

Delivery object

json
1{
2 "id": "wd_2b6f...",
3 "endpoint_id": "we_8f3a...",
4 "job_id": "c315f78f-d2c7-4541-b240-a9372842de94",
5 "event_type": "render.succeeded",
6 "status": "pending",
7 "http_status": 200,
8 "attempt": 1,
9 "last_error": null,
10 "created_at": "2026-06-21T10:00:01Z",
11 "updated_at": null
12}
Manage endpoints with an API key
1# Create an endpoint subscribed to all events (empty event_types)
2curl -X POST "https://api.sudomock.com/api/v1/webhook-endpoints" \
3 -H "Content-Type: application/json" \
4 -H "x-api-key: sm_your_api_key" \
5 -d '{
6 "url": "https://your-domain.com/hooks/sudomock",
7 "description": "Production listener",
8 "event_types": []
9 }'
10
11# Send a test delivery through the signed path
12curl -X POST "https://api.sudomock.com/api/v1/webhook-endpoints/we_8f3a.../test" \
13 -H "x-api-key: sm_your_api_key"

Store the secret on create

The full whsec_ secret is returned only once, on create and on rotate-secret. Save it when you receive it; afterwards it is always masked. You need it to verify signatures (see above).

Retries & replay

Failed deliveries are retried automatically. You can inspect every attempt (with its HTTP status, attempt count and error) and replay a single delivery from the deliveries panel in your dashboard. Deliveries are idempotent: handle the same job_id + event safely on your side. A replayed delivery may arrive with result_url set to null; when that happens, fetch the result by polling GET /api/v1/jobs/{job_id}.