Webhooks
Get notified the moment a run finishes instead of polling. Pass webhookUrl when you create a run and Photopipe POSTs a JSON payload to that URL once the run reaches a terminal status.
When events fire
Exactly one POST is sent per run, the first time the run reaches one of these terminal statuses:
completed— every item succeeded.partial— some items succeeded, some failed.failed— every item failed (or the run never produced any).cancelled— the run was cancelled via the API or UI.
Subsequent status updates (e.g. cancelling an already-failed run) do not produce additional events.
Subscribing
Pass webhookUrl in the body of POST /workflows/:id/runs. The URL is stored on the run record and used for that run only — there's no global subscription concept; every run carries its own callback. The URL must use https:// and is capped at 1024 characters.
{
"inputs": { "imageInput-1": { "files": [ /* ... */ ] } },
"webhookUrl": "https://your-app.example.com/hooks/photopipe-run"
}Request shape
Photopipe sends a JSON POST. The body is intentionally minimal — treat it as a wake-up signal and call back into the API (Get a run, Download outputs) for per-item details and signed download URLs.
POST https://your-app.example.com/hooks/photopipe-run
Content-Type: application/json
X-Photopipe-Event: run.finished
X-Photopipe-Run-Id: 9871
{
"event": "run.finished",
"runId": 9871,
"workflowId": 123,
"workspaceId": 42,
"status": "completed",
"totalItems": 2,
"completedItems": 2,
"failedItems": 0,
"startedAt": "2026-04-29T14:05:01.000Z",
"finishedAt": "2026-04-29T14:05:42.000Z",
"error": null
}Content-TypeX-Photopipe-EventX-Photopipe-Run-IdeventrunIdworkflowIdworkspaceIdstatustotalItemscompletedItemsfailedItemsstartedAtfinishedAterrorResponding
Return any 2xxstatus within 15 seconds to acknowledge the delivery. Anything else — non-2xx, timeout, DNS failure, TLS error — is treated as a failure and the message is re-queued.
We don't inspect the response body, so an empty 200 OK is fine. Keep your handler fast: do the minimum work needed to record the notification (e.g. enqueue an internal job) and return.
Retries & delivery guarantees
Delivery is at-least-once. Failed deliveries are retried with exponential backoff:
Attempt 1Attempt 2Attempt 3Attempt 4Attempt 5Attempt 6After the retry budget is exhausted the event is dropped and will not be retried. To recover from an extended outage, call Get a runfor any in-flight runs you remember dispatching — the run record holds the same status information indefinitely.
Because retries can land out of order with new events from unrelated runs, dedupe on X-Photopipe-Run-Id + status. Once you've processed an event for a given runId, ignore subsequent deliveries for the same id.
Authenticating the request
Photopipe currently does not sign webhook bodies. To verify the request is genuine, embed a secret token directly in your webhookUrl path or query string and check it on receipt:
"webhookUrl": "https://your-app.example.com/hooks/photopipe-run?secret=YOUR_SHARED_SECRET"Treat the URL itself as a credential — rotate it if you suspect it has leaked. HMAC body signing is on the roadmap.
Local development
Plain http:// URLs are rejected at create-run time, so a local Express server on http://localhost:3000 won't work directly. Use a tunneling tool such as ngrok or Cloudflare Tunnel to expose your local handler over HTTPS while developing.