API
esc

Type to search.

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
Terminal window
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"
}'
201 Created
{
"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.

Request body
{
"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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t 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/webhooks and GET/v1/webhooks/{id} read them back, without the secret.
  • PATCH/v1/webhooks/{id} changes events, description or status (active or paused). Pass rotate_secret: true to 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.