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
whsec_), shown once when you create or rotate it.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. |
| webhook.test | Fired 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
1X-SudoMock-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a082X-SudoMock-Timestamp: 17189000003Content-Type: application/json
Body
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.
1import crypto from 'crypto'23// 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')89 // Reject replays older than 5 minutes.10 const age = Math.floor(Date.now() / 1000) - Number(timestamp)11 if (!timestamp || Math.abs(age) > 300) return false1213 const expected = crypto14 .createHmac('sha256', secret)15 .update(`${timestamp}.${rawBody}`)16 .digest('hex')1718 return crypto.timingSafeEqual(19 Buffer.from(signature || '', 'hex'),20 Buffer.from(expected, 'hex')21 )22}
Always verify before trusting a payload
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 & path | What it does |
|---|---|
| POST /api/v1/webhook-endpoints | Create an endpoint. Body: { url (https), description?, event_types[] }. Returns 201 with the full whsec_ secret — shown once. |
| GET /api/v1/webhook-endpoints | List 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-secret | Issue a new signing secret. Returns the full whsec_ secret once. |
| POST /api/v1/webhook-endpoints/{id}/test | Send a synthetic webhook.test through the real signed path. Returns 202 { status, event_type, job_id }. |
| GET /api/v1/webhook-endpoints/{id}/deliveries | List delivery attempts for one endpoint. Query: status?, event_type?, limit (1-200, default 50). |
| GET /api/v1/webhook-endpoints/events | Recent 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}/replay | Replay a single delivery with the same idempotency key. Returns 202 { status, delivery_id }. |
| POST /api/v1/webhook-endpoints/{id}/deliveries/replay-failed | Replay 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.
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": null10}
Delivery object
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": null12}
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 }'1011# Send a test delivery through the signed path12curl -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
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}.