An outbound webhook is an HTTP POST that RichAPI sends to a URL of yours when a job reaches a final state. RichAPI signs the payload with HMAC-SHA256, retries a failed delivery up to 3 attempts, and keeps a delivery record for 30 days. Register a webhook with the RichAPI API. The dashboard holds no webhook screen.
Where do I manage RichAPI outbound webhooks?
RichAPI outbound webhooks are API-only. No page, card, or button for outbound webhooks exists in the dashboard. Do not look for one. Every task in the table below is an HTTP request that carries your API key.
Task | Request |
List your webhooks |
|
Register a webhook |
|
Delete a webhook |
|
Read the delivery log |
|
Send a test event |
|
Send a past delivery again |
|
API detail | Webhook API |
Base URL |
|
Authentication |
|
Response | Synchronous JSON |
Credit cost | 0 credits |
A webhook belongs to the API key that registered it. RichAPI sends events for jobs that the same key started, and the list request returns the webhooks of that key only. After you revoke a key, register the webhook again with the new key.
Why use an outbound webhook instead of a poll?
A poll asks RichAPI for the state of a job again and again. Every poll is a request that your client must send, time, and handle. Most polls return the same unfinished state, so the work produces nothing.
An outbound webhook reverses the direction. RichAPI sends one POST to your server within seconds of the final state, and your server does no waiting. A webhook removes the poll loop, the poll interval, and the delay between the true finish time and your next poll.
Which RichAPI endpoints send outbound webhooks?
RichAPI sends job.completed and job.failed from one place only. A job status poll that returns a final status fires the event. An endpoint that returns its result in the same response creates no job, so that endpoint sends no event.
Endpoint kind | How you read the result | Outbound webhook events |
Synchronous endpoint | In the response of the same request | None |
Asynchronous job endpoint | Poll the job status path with the |
|
Most endpoints documented in this help center are synchronous. The bulk jobs email_finding and email_verification take arrays and run as jobs. Each synchronous endpoint accepts a POST, runs the work, and returns the data in the response of that request. For a synchronous endpoint, read the response body and skip webhooks altogether.
CAUTION: A synchronous endpoint sends no outbound webhook. Do not hold a record open and wait for an event that never arrives. Read the response of the call.
What events does RichAPI send?
Event | When RichAPI sends the event |
| A job status poll returns |
| A job status poll returns |
| Registration accepts this name, and RichAPI fires no delivery for it today. |
RichAPI reads the status value of the poll response and maps it to one of the two job events.
Status in the poll response | Event RichAPI sends |
|
|
|
|
|
|
|
|
|
|
|
|
A poll that returns any other status, for example a status that means unfinished work, fires no event.
CAUTION: Do not wait for a quota.exceeded delivery. When a workspace runs out of credits, the API answers the call itself with a 402 status and an Insufficient credits error. Watch for that status code, and turn on auto-reload.
What does a RichAPI webhook payload look like?
The payload is one JSON object. RichAPI sends it as the raw body of an HTTP POST with the header Content-Type: application/json. The JSON is compact, so it carries no spaces and no line breaks.
The example below is the payload of a test event, formatted for reading. A test event is the one payload that you can reproduce on demand, so the values are exact.
{
"event": "job.completed",
"api_slug": "test_endpoint",
"timestamp": "2026-08-10T09:15:30.123456+00:00",
"job_id": "test-job-id-00000000",
"status": "completed",
"result": {
"test": true
},
"credits_consumed": 1.0
}
In a real job event, api_slug names the API that produced the job. The result object then holds the full job status response.
Field | Type | Always present | Description |
| string | Yes | The event name: |
| string | Yes | The name of the API that produced the job. |
| string | Yes | ISO 8601 UTC time of the payload. |
| string | No | The job identifier. RichAPI omits the field when no job identifier exists. |
| string | No | The final status from the poll response, in lower case. |
| object | No | The full poll response, including the job data. |
| number | No | The credits calculated for the job result. |
RichAPI omits job_id, status, result, and credits_consumed when the value is empty. Read every one of those four fields with a safe accessor.
How do I register a webhook endpoint?
Build an HTTPS endpoint on your server that accepts an HTTP POST.
Generate a long random string as the signing secret.
Store the secret on your server. Never put it in client code.
Send the registration request below with the URL, the events, and the secret.
Read
webhook_idfrom the201response and store it.Send a test event to that
webhook_id.Read the delivery log and confirm one record with
status: "delivered".
cURL
curl -X POST https://api.richapi.ai/api/v1/outbound-webhooks
-H "x-api-key: YOUR_API_KEY"
-H "Content-Type: application/json"
-d '{
"name": "Production job events",
"url": "https://hooks.example.com/richapi",
"events": ["job.completed", "job.failed"],
"secret": "a-long-random-signing-secret"
}'
Python
import requests
response = requests.post(
"https://api.richapi.ai/api/v1/outbound-webhooks",
headers={"x-api-key": "YOUR_API_KEY"},
json={
"name": "Production job events",
"url": "https://hooks.example.com/richapi",
"events": ["job.completed", "job.failed"],
"secret": "a-long-random-signing-secret",
},
timeout=60,
)
response.raise_for_status()
print(response.json())
Node.js
const response = await fetch("https://api.richapi.ai/api/v1/outbound-webhooks", {
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Production job events",
url: "https://hooks.example.com/richapi",
events: ["job.completed", "job.failed"],
secret: "a-long-random-signing-secret",
}),
});
const data = await response.json();
console.log(data);
Registration parameters
Parameter | Type | Required | Default | Description |
| string | Yes | — | Your label for the webhook. |
| string | Yes | — | The HTTPS URL that receives the POST. |
| array of strings | Yes | — | One or more of |
| string | No | none | The HMAC signing secret. Without a secret, RichAPI sends no signature header. |
| array of strings | No | empty | Limits deliveries to the named APIs. An empty list matches every API. |
A successful registration returns status 201 and the identifier of the new webhook.
{ "webhook_id": "550e8400-e29b-41d4-a716-446655440000" }
How does RichAPI sign a webhook payload?
RichAPI signs a payload only when the webhook carries a secret. RichAPI computes an HMAC-SHA256 of the exact bytes of the request body, keyed with your secret, and sends the hexadecimal digest in one header.
X-RichAPI-Signature: sha256=8f2b1c...
Signature part | What RichAPI uses |
Header name |
|
Algorithm | HMAC-SHA256 |
Key | The |
Signed data | The raw request body, byte for byte |
Header format |
|
X-RichAPI-Signature is the only signature header that RichAPI sends. A webhook with no secret arrives with no signature header at all.
WARNING: Sign nothing but the raw body. A parsed and re-serialized body produces a different byte string and a wrong signature. Read the raw bytes before your framework parses the JSON.
How do I verify a RichAPI webhook signature?
Read the raw request body as bytes, before any JSON parse.
Read the
X-RichAPI-Signatureheader. Treat a missing header as a failure.Compute
sha256=plus the HMAC-SHA256 hexadecimal digest of the raw body.Compare the two strings with a constant-time comparison.
On a mismatch, return
401and process nothing.
Python
import hashlib
import hmac
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = "a-long-random-signing-secret"
def is_valid_signature(raw_body: bytes, header_value: str | None) -> bool:
if not header_value:
return False
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header_value)
@app.post("/richapi")
def receive_webhook():
raw_body = request.get_data()
signature = request.headers.get("X-RichAPI-Signature")
if not is_valid_signature(raw_body, signature):
return "", 401
payload = request.get_json()
print(payload["event"], payload.get("job_id"))
return "", 200
Node.js
const crypto = require("node:crypto");
const express = require("express");
const app = express();
const WEBHOOK_SECRET = "a-long-random-signing-secret";
function isValidSignature(rawBody, headerValue) {
if (!headerValue) {
return false;
}
const expected =
"sha256=" +
crypto.createHmac("sha256", WEBHOOK_SECRET).update(rawBody).digest("hex");
const expectedBytes = Buffer.from(expected, "utf8");
const receivedBytes = Buffer.from(headerValue, "utf8");
if (expectedBytes.length !== receivedBytes.length) {
return false;
}
return crypto.timingSafeEqual(expectedBytes, receivedBytes);
}
app.post("/richapi", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.header("X-RichAPI-Signature");
if (!isValidSignature(req.body, signature)) {
res.status(401).end();
return;
}
const payload = JSON.parse(req.body.toString("utf8"));
console.log(payload.event, payload.job_id);
res.status(200).end();
});
app.listen(3000);
WARNING:crypto.timingSafeEqual throws an error on two buffers of different length. Compare the lengths first, as the sample above does. A thrown error inside your handler returns a 500 and costs you the delivery.
The Python sample reads the raw body with request.get_data(). The Node.js sample reads the raw body with express.raw. A JSON body parser destroys the raw bytes, so do not put express.json() in front of this route.
What must my webhook endpoint return?
Requirement | Rule |
Status code for a success | Any code from |
Time limit for the response | 10 seconds per attempt |
Redirect responses | Not followed. A |
Body of the response | Not read. RichAPI ignores it. |
Return the 2xx status first and do the work afterwards. Write the payload to a queue, answer RichAPI, and process the queue in a background worker. A handler that enriches, writes, and answers inside the 10-second limit fails on a slow day.
Handle the same event more than once. A delivery that succeeds after a timeout arrives twice, so store the job_id of every event that you process and drop a repeat.
How does RichAPI retry a failed delivery?
RichAPI makes up to 3 attempts for one event. Any status outside 200 to 299 counts as a failure. A timeout, a network error, a refused redirect, and a blocked URL also count as failures.
Attempt | Delay before the attempt |
1 | None |
2 | About 5 to 7 seconds |
3 | About 25 to 27 seconds |
RichAPI adds up to 2 seconds of random jitter to each delay. After the third failed attempt, RichAPI marks the delivery failed and stops. RichAPI records every attempt in the delivery log, including the successful one.
To send a failed event again, fix your endpoint and then send a redelivery request.
curl -X POST https://api.richapi.ai/api/v1/outbound-webhooks/WEBHOOK_ID/redeliver/DELIVERY_ID
-H "x-api-key: YOUR_API_KEY"
A redelivery repeats the stored payload, with the original timestamp, and signs it with the current secret. A redelivery runs the same 3-attempt schedule.
Which webhook URLs does RichAPI accept?
Rule | Detail |
Scheme |
|
Address | Every address that the hostname resolves to must be public and routable. |
Blocked addresses | Private, loopback, link-local, reserved, multicast, and unspecified addresses. |
Redirects | Not followed. Register the final URL. |
Check time | At registration, and again before every delivery attempt. |
A URL that fails these rules is rejected at registration with status 400 and this error.
{ "error": "url must be HTTPS and point to a public address" }
The address check runs again before each delivery attempt. If DNS for your hostname later resolves to a private address, RichAPI sends nothing and records the attempt as a failure with no HTTP code.
CAUTION: A tunnel to localhost and an address such as 127.0.0.1 or 169.254.169.254 are always rejected. For a local test, expose your server through a public HTTPS tunnel URL.
How do I test my webhook endpoint?
Register the webhook and store the
webhook_id.Send the test request below.
Read the
payloadobject in the response. That object is the exact body sent.Read the delivery log and check the
statusof the newest record.If the status is
failed, correct your endpoint and send the test again.
curl -X POST https://api.richapi.ai/api/v1/outbound-webhooks/WEBHOOK_ID/test
-H "x-api-key: YOUR_API_KEY"
The test fires a job.completed payload with the API name test_endpoint, the job identifier test-job-id-00000000, and credits_consumed of 1.0. The result object of a test payload holds test: true and a fixed message string.
To test the failure path instead, name the event in the request body.
curl -X POST https://api.richapi.ai/api/v1/outbound-webhooks/WEBHOOK_ID/test
-H "x-api-key: YOUR_API_KEY"
-H "Content-Type: application/json"
-d '{"event": "job.failed"}'
CAUTION: The test request answers {"status": "test_fired"} even after every attempt fails. The response reports the send, not the outcome. Read the delivery log for the outcome.
How do I read the webhook delivery log?
curl "https://api.richapi.ai/api/v1/outbound-webhooks/WEBHOOK_ID/deliveries?status=failed&limit=25"
-H "x-api-key: YOUR_API_KEY"
Parameter | Type | Required | Default | Description |
| string | No | all | Filters on |
| string | No | all | Filters on one event name. |
| string | No | none | ISO 8601 time. Returns records at or after this time. |
| string | No | none | ISO 8601 time. Returns records at or before this time. |
| integer | No | 50 | Records per page. The maximum is 100. |
| string | No | none | The |
The response holds the records, newest first, and a cursor when more records exist.
Field | Type | Description |
| string | The identifier of this attempt. Use it for a redelivery. |
| string | The webhook that made the attempt. |
| string | The event name of the payload. |
| string |
|
| integer | The status your server returned. The value is |
| integer | The round-trip time in milliseconds. |
| integer | 1 for the first try, 3 for the last retry. |
| string | The exact JSON body that RichAPI sent, as a string. |
| number | The time of the attempt, as a Unix timestamp. |
| string | The time RichAPI removes the record. |
| string | The API key that owns the webhook. |
RichAPI keeps a delivery record for 30 days and then removes it. Copy anything you need for a longer audit into your own store.
How do I list, change, or delete a webhook?
To list every webhook of the current API key, send a GET.
curl https://api.richapi.ai/api/v1/outbound-webhooks
-H "x-api-key: YOUR_API_KEY"
The list returns webhook_id, name, url, events, api_slugs, created_at, and the identifier of the owning API key. RichAPI never returns the secret of a webhook. Store your own copy of the secret.
RichAPI accepts no update request for a registered webhook. To change a URL, an event list, or a secret, delete the webhook and register a new one.
curl -X DELETE https://api.richapi.ai/api/v1/outbound-webhooks/WEBHOOK_ID
-H "x-api-key: YOUR_API_KEY"
A successful delete returns {"deleted": "WEBHOOK_ID"}. The delete removes the configuration. Delivery records of that webhook stay readable until they expire.
How much do RichAPI outbound webhooks cost?
Outbound webhooks charge 0 credits. Registration, a list request, a delete, a test event, a redelivery, and every delivery attempt are free.
Action | Credits |
Register a webhook | 0 |
List webhooks | 0 |
Delete a webhook | 0 |
Send a test event | 0 |
Send a redelivery | 0 |
Each delivery attempt | 0 |
RichAPI charges credits for successful API requests only. The credits_consumed field in a payload reports the credits calculated for that job, and the delivery itself adds nothing.
Troubleshooting outbound webhooks
What you see | Cause | What to do |
| The key is absent, wrong, or revoked. | Send the current key in the |
| The URL is not HTTPS, or the hostname resolves to a private address. | Register a public HTTPS URL. |
| The request body carries no | Send both fields. |
| The | Send a list with at least one event name. |
| An event name is outside the three supported names. | Send only |
| The body is not valid JSON. | Correct the JSON and send the request again. |
| The | Use |
| The | Send the |
| The identifier is wrong, or the webhook belongs to another API key. | List the webhooks with the same key and copy the identifier. |
| The delivery identifier is wrong or expired. | Read the delivery log and copy a current |
| The request used a method that the route does not accept. | Use |
A delivery is | A timeout, a network error, or a blocked URL. | Check public DNS, the TLS certificate, and the 10-second limit. |
A delivery is | Your endpoint redirects the POST. | Register the final URL, then delete the old webhook. |
A delivery is | Your handler raised an error. | Fix the handler, then send a redelivery. |
Deliveries arrive with no signature header | The webhook carries no secret. | Delete the webhook and register it again with a |
The signature never matches | Your code signs a re-serialized body. | Sign the raw request bytes. |
No event ever arrives | The endpoint you call is synchronous, so it creates no job. | Read the result in the response of the call. |
If a failure repeats, contact [email protected]. Name the webhook_id and one delivery_id. Never send the signing secret or the full API key.
Frequently asked questions
Does the RichAPI dashboard have a webhook page?
No. RichAPI outbound webhooks are API-only. Register, list, test, and delete a webhook with requests to https://api.richapi.ai/api/v1/outbound-webhooks, and send your API key in the x-api-key header.
Which signature header does RichAPI send?
X-RichAPI-Signature. The value is sha256= plus the HMAC-SHA256 hexadecimal digest of the raw request body, keyed with the secret of the webhook. A webhook registered with no secret receives no signature header.
How many times does RichAPI retry a failed webhook delivery?
RichAPI makes 3 attempts in total: one immediate attempt, one after about 5 seconds, and one after about 25 seconds. After the third failure, RichAPI marks the delivery failed and stops.
What status code must my webhook endpoint return?
Any status from 200 to 299, within 10 seconds. RichAPI ignores the response body. A redirect, a timeout, and any other status count as a failure and start a retry.
Do RichAPI outbound webhooks cost credits?
No. Registration, test events, redeliveries, and delivery attempts all charge 0 credits. RichAPI charges credits for successful API requests only.
Can I change the URL of a RichAPI webhook?
No. RichAPI accepts no update request for a registered webhook. Delete the webhook and register a new one with the correct URL, event list, and secret.
Does RichAPI send a webhook when my credits run out?
No. RichAPI fires no quota.exceeded delivery today. When credits run out, the API call itself returns status 402 with an Insufficient credits error, so watch for that status code.
How long does RichAPI keep webhook delivery records?
30 days. After 30 days, RichAPI removes the record, and a redelivery of it becomes impossible. Copy the records you need into your own store.
