Getting started
Webhooks
Get told when a job finishes instead of polling for it.
Register an endpoint in the dashboard, or pass webhook_url on any request to override it for that job. We POST the full job object when it reaches a terminal state.
{
"id": "evt_01HQ8W12PN4R7C",
"type": "job.succeeded",
"created_at": "2026-09-18T09:14:11Z",
"data": {
"id": "job_01HQ8VZ3K9XM2P",
"object": "job",
"type": "image",
"status": "succeeded",
"images": [
{
"id": "img_01HQ8VZ4T2B7MC",
"url": "https://cdn.journeyapi.dev/i/01HQ8VZ4T2B7MC.png",
"width": 1456,
"height": 972
}
],
"cost_usd": 0.05
}
}Event types
| Event | Fires when |
|---|---|
| job.succeeded | A job completed and its results are retrievable. |
| job.failed | A job failed. The refund event follows separately. |
| job.cancelled | A job was cancelled before completion. |
| balance.low | Your balance crossed the threshold set in the dashboard. |
Verifying the signature
Every delivery carries an Journey-Signature header: a timestamp and an HMAC-SHA256 of the raw body, keyed with your webhook secret. Compare in constant time and reject a timestamp older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
if (a.length !== b.length || !timingSafeEqual(a, b)) return false;
return Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
}Retries
A delivery that does not return a 2xx within ten seconds is retried eight times with exponential backoff, over roughly nine hours. Deliveries are at-least-once, so key on the event id and ignore a repeat.