Webhooks
Register an HTTPS endpoint once and receive a signed event whenever a task, import or export reaches a terminal state. Retried for six hours; deduplicate on id.
Register an endpoint
POST/v1/webhooks
curl https://api.veeton.com/v1/webhooks \ -H "Authorization: Bearer $VEETON_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your.app/veeton", "events": ["task.succeeded", "task.failed", "import.completed", "import.failed"], "description": "Production PIM" }'{ "id": "01HX5K2MZ7A3Q4FBNDC0EVDXYW", "url": "https://your.app/veeton", "events": ["task.succeeded", "task.failed", "import.completed", "import.failed"], "status": "active", "description": "Production PIM", "last_delivery_at": null, "created_at": "2026-09-03T14:30:09Z", "signing_secret": "whsec_2c7f9b41…"}The url must be a public https:// endpoint on the default port without embedded credentials. It is verified once here and is immutable afterwards; create a new webhook to change it.
Events
| Event | Fires when |
|---|---|
task.succeeded, task.failed |
A beautifier or tryon task reaches a terminal state |
import.completed, import.failed |
An import job finishes. An import with some failed rows still completed; fetch GET /v1/imports/{id} for the per-row picture. |
export.completed, export.failed |
An export job finishes. Fetch GET /v1/exports/{id} for the signed artifact URL. |
The delivery
Each event is a POST with a JSON body and a signature header.
{ "id": "evt_5f1c3a9e2b7d4e0f8a6c1b2d3e4f5a6b", "type": "task.succeeded", "created_at": "2026-09-03T14:31:40Z", "data": { "task": { "id": "01HX5K2MZ7A3Q4FBNDC0EVDXY2", "status": "succeeded" } }}The body names the resource and its status and nothing more. Fetch the resource for the rest: the output image, the counters, the artifact URL. That keeps a webhook payload safe to log and means a late-arriving event never carries stale detail.
data is keyed by resource: data.task, data.import or data.export.
Verify the signature
Veeton-Signature: t=1756909900,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt is the Unix time the signature was made; v1 is the hex HMAC-SHA256 of "{t}.{raw_body}" keyed by your signing_secret. Recompute it over the raw request body, compare in constant time, and reject timestamps older than a few minutes.
import { createHmac, timingSafeEqual } from "node:crypto"
export function verify(header: string, rawBody: string, secret: string, toleranceSeconds = 300): boolean { const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string])) const t = Number(parts.t) if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex") const given = parts.v1 ?? "" return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected))}import hmac, hashlib, time
def verify(header: str, raw_body: bytes, secret: str, tolerance_seconds: int = 300) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) try: t = int(parts["t"]) except (KeyError, ValueError): return False if abs(time.time() - t) > tolerance_seconds: return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(parts.get("v1", ""), expected)Return a 2xx as soon as you have accepted the event. Do the work afterwards: handlers slower than 10 seconds are timed out and retried.
Retries and duplicates
A delivery that fails (non-2xx, timeout, unreachable host) is retried on a backoff of roughly 1 minute, 5 minutes, 15 minutes, 1 hour and 6 hours, then given up on.
Delivery is at-least-once. A retry of a delivery whose response we never recorded looks identical to the first attempt. The id field (evt_…) is stable across every attempt of the same event, so treat it as the idempotency key on your side and ignore an id you have already processed.
Polling GET /v1/tasks, GET /v1/imports/{id} and GET /v1/exports/{id} is always available for recovery after downtime.
Manage endpoints
- GET
/v1/webhooksand GET/v1/webhooks/{id}read them back, without the secret. - PATCH
/v1/webhooks/{id}changesevents,descriptionorstatus(activeorpaused). Passrotate_secret: trueto mint a new secret, returned once in that response; the previous one stops working immediately, so plan for a brief gap if your receiver verifies synchronously during a deploy. - DELETE
/v1/webhooks/{id}removes it.