Skip to content

Receive results with a webhook

Updated
Reading time
3 min
Level
intermediate
Needs
An HTTPS endpoint reachable from the internet

Supply callback_url and Revoye Cloud POSTs the finished job to it, so your code does not have to hold a connection open for a minute or poll. This is the right shape for anything unattended: batch work, scheduled jobs, or a queue worker.

curl $REVOYE_BASE_URL/v1/completions \
  -H "Authorization: Bearer $REVOYE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "prompt": "Write release notes for this changelog: …",
    "wait": false,
    "callback_url": "https://your-app.example.com/hooks/revoye",
    "metadata": { "release_id": "r_2291" }
  }'

You get the job back immediately with 202. When the job reaches a final status — succeeded, failed, cancelled or expired — Revoye Cloud delivers it to your endpoint.

The delivery

POST /hooks/revoye HTTP/1.1
Content-Type: application/json
User-Agent: revoye-webhooks/1
X-Revoye-Signature: sha256=<hex>
X-Revoye-Timestamp: 1758186861
 
{
  "id": "job_01JAY7Q2K8XYZ3M4N5P6Q7R8S9",
  "status": "succeeded",
  "response": "…",
  "provider": null,
  "agent_id": "agt_01JAY7…",
  "conversation_ref": "<opaque string>",
  "attempts": 1,
  "queue_ms": 240,
  "run_ms": 18432,
  "created_at": "2026-09-18T09:14:02.000Z",
  "finished_at": "2026-09-18T09:14:21.000Z",
  "metadata": { "release_id": "r_2291" }
}

The body carries the same fields as the job object, except agent_name, content_pruned_at and queue_position. Your metadata arrives unchanged, which is how you route the delivery without a database lookup. Check status before using response: a failed, cancelled or expired job is delivered too, with response: null.

Trust, but confirm

Your URL is public; anything can POST to it. Each delivery carries an X-Revoye-Timestamp header (Unix seconds) and an X-Revoye-Signature header, but the details for verifying that signature are not published yet. Until they are, treat a delivery as a notification, not as the result:

  1. Read id from the body.
  2. Fetch GET /v1/completions/{id} with your API key.
  3. Act on what that returns.

A forged delivery then costs you one GET that returns 404, and nothing else. Also reject any delivery whose timestamp is more than five minutes from your clock, and use an unguessable path for the endpoint.

Delivery is at-least-once

A delivery succeeds when your endpoint answers 2xx within 10 seconds. Anything else — another status, a timeout, a refused connection — is retried: five attempts in all, the first immediately and then after 30 seconds, 5 minutes, 30 minutes and 2 hours. After the fifth failure Revoye Cloud stops trying, and the result stays available at GET /v1/completions/{id}.

Your endpoint must be idempotent. At-least-once is what survives a network; exactly-once is not on offer from anyone who is being honest. Key your handler on the job id, record it, and make a second delivery of the same id a no-op:

if already_processed(job["id"]):
    return 200
process(job)
mark_processed(job["id"])
return 200

Requirements for your endpoint

SchemeHTTPS only. An http:// callback_url is rejected at submission with 400 INVALID_REQUEST
AddressMust resolve to a public internet address. Private, loopback and link-local addresses are never called
RedirectsNot followed. A 3xx counts as a failed attempt
Response2xx within 10 seconds. The response body is ignored
Slow handlerCounts as a failure and earns a retry — so acknowledge first, work second

Webhook or long poll?

UseWhen
wait: trueInteractive work, a script you are watching, development. Simplest to write
wait: false + callback_urlEverything unattended. Batches, schedules, queue workers, anything where "the model is busy" should mean "later" and not "error"
wait: false + polling GETOnly if you genuinely cannot receive an inbound request. Poll at intervals of seconds, not milliseconds