Webhooks
Instead of polling GET /jobs/:id, you can have snapnedit push a signed HTTP POST to a URL you own every time one of your jobs reaches a terminal state. Register an endpoint from your dashboard (any account with an API key can), pick the events you care about, and verify the signature on each delivery.
Events
An endpoint subscribes to one or more event types:
job.succeeded— a job finished and its output is ready.job.failed— a job failed permanently.
The set of event types is designed to grow, so treat an unrecognized type as a no-op rather than an error.
Delivery payload
Every delivery is a JSON body of the shape { id, type, created, data }, where id is the delivery id, type is the event type, created is unix seconds, and data depends on the event.
A job.succeeded delivery:
{
"id": "whd_9f8a...",
"type": "job.succeeded",
"created": 1721433600,
"data": {
"jobId": "job_123",
"operation": "remove-background",
"status": "succeeded",
"outputAssetId": "asset_789",
"download": "https://.../signed-download-url"
}
}
A job.failed delivery:
{
"id": "whd_1c2d...",
"type": "job.failed",
"created": 1721433605,
"data": {
"jobId": "job_124",
"operation": "upscale",
"status": "failed",
"errorCode": "provider_failed",
"message": "the upstream provider returned an error"
}
}
The download field on a success is an optional, short-lived signed URL — treat it as expiring and re-download promptly, or fetch a fresh one with GET /jobs/:jobId.
Headers
Each POST carries these headers alongside Content-Type: application/json:
X-Snapnedit-Signature: t=<unixSeconds>,v1=<hex>
X-Snapnedit-Event: job.succeeded
X-Snapnedit-Delivery: whd_9f8a...
Verifying the signature
X-Snapnedit-Signature is Stripe-style: t=<unixSeconds>,v1=<hex>, where <hex> is the HMAC-SHA256 of the string `<t>.<rawBody>` — the signed timestamp, a literal ., then the exact raw request body — keyed by your endpoint's signing secret (whsec_snap_...).
Verify against the raw request bytes, before parsing JSON — a re-serialized body may not match byte-for-byte. Compare in constant time, and (optionally) reject deliveries whose timestamp is too far from now to blunt replay attacks.
With the SDK
@snapnedit/sdk ships a typed, dependency-free helper (Web Crypto under the hood, so it runs in Node and the browser/edge):
import { verifyWebhookSignature } from '@snapnedit/sdk';
import type { WebhookDeliveryBody } from '@snapnedit/sdk';
// e.g. a Next.js route handler
export async function POST(request: Request): Promise<Response> {
const rawBody = await request.text(); // raw, unparsed
const signature = request.headers.get('x-snapnedit-signature') ?? '';
const ok = await verifyWebhookSignature(rawBody, signature, process.env.SNAPNEDIT_WEBHOOK_SECRET!, {
toleranceSeconds: 300, // optional replay protection
});
if (!ok) return new Response('bad signature', { status: 400 });
const event = JSON.parse(rawBody) as WebhookDeliveryBody;
// ...handle event.type / event.data
return new Response(null, { status: 204 });
}
With plain Node
No SDK required — node:crypto reproduces the same digest:
import { createHmac, timingSafeEqual } from 'node:crypto';
/** Verifies an `X-Snapnedit-Signature` header against the raw body + endpoint secret. */
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = parts.t;
const v1 = parts.v1;
if (!t || !v1) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(v1, 'utf8');
return a.length === b.length && timingSafeEqual(a, b);
}
Respond with any 2xx once you've accepted the delivery; anything else (or a timeout) is treated as a failed attempt and retried.
Retries and backoff
Deliveries are queued durably and retried up to 5 attempts with exponential backoff. A delivery that returns a 2xx is marked succeeded; a non-2xx response or a network error reschedules the next attempt, and once attempts are exhausted the delivery is marked failed. Because a delivery can be retried, your handler should be idempotent — de-duplicate on the delivery id (or the X-Snapnedit-Delivery header). You can review recent delivery attempts, with their status codes, per endpoint in the dashboard.
Managing endpoints
From the dashboard webhooks section you can:
- Create an endpoint by entering an
https://URL and choosing its events. The signing secret (whsec_snap_...) is shown exactly once, at creation — copy it then; it can't be retrieved again. - Rotate the secret if it leaks. A fresh secret is shown once and the old one stops signing immediately.
- Disable / enable an endpoint to pause deliveries without deleting it, or delete it entirely.
Endpoint URLs must be public https:// URLs; localhost, private, and internal addresses are rejected.