journeyapi.devDocs
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.

Delivered payload
{
  "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

EventFires when
job.succeededA job completed and its results are retrievable.
job.failedA job failed. The refund event follows separately.
job.cancelledA job was cancelled before completion.
balance.lowYour 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.

Verification
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.