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:
- Read
idfrom the body. - Fetch
GET /v1/completions/{id}with your API key. - 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 200Requirements for your endpoint
| Scheme | HTTPS only. An http:// callback_url is rejected at submission with 400 INVALID_REQUEST |
| Address | Must resolve to a public internet address. Private, loopback and link-local addresses are never called |
| Redirects | Not followed. A 3xx counts as a failed attempt |
| Response | 2xx within 10 seconds. The response body is ignored |
| Slow handler | Counts as a failure and earns a retry — so acknowledge first, work second |
Webhook or long poll?
| Use | When |
|---|---|
wait: true | Interactive work, a script you are watching, development. Simplest to write |
wait: false + callback_url | Everything unattended. Batches, schedules, queue workers, anything where "the model is busy" should mean "later" and not "error" |
wait: false + polling GET | Only if you genuinely cannot receive an inbound request. Poll at intervals of seconds, not milliseconds |