Skip to main content

Receiving Outbound Webhooks

Written by Kadri - RichAPI Support Team

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

GET /outbound-webhooks

Register a webhook

POST /outbound-webhooks

Delete a webhook

DELETE /outbound-webhooks/{webhook_id}

Read the delivery log

GET /outbound-webhooks/{webhook_id}/deliveries

Send a test event

POST /outbound-webhooks/{webhook_id}/test

Send a past delivery again

POST /outbound-webhooks/{webhook_id}/redeliver/{delivery_id}

API detail

Webhook API

Base URL

https://api.richapi.ai/api/v1

Authentication

x-api-key header

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 job_id

job.completed or job.failed on the final poll

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

job.completed

A job status poll returns completed, done, or success.

job.failed

A job status poll returns failed, error, or cancelled.

quota.exceeded

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

completed

job.completed

done

job.completed

success

job.completed

failed

job.failed

error

job.failed

cancelled

job.failed

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

event

string

Yes

The event name: job.completed or job.failed.

api_slug

string

Yes

The name of the API that produced the job.

timestamp

string

Yes

ISO 8601 UTC time of the payload.

job_id

string

No

The job identifier. RichAPI omits the field when no job identifier exists.

status

string

No

The final status from the poll response, in lower case.

result

object

No

The full poll response, including the job data.

credits_consumed

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?

  1. Build an HTTPS endpoint on your server that accepts an HTTP POST.

  2. Generate a long random string as the signing secret.

  3. Store the secret on your server. Never put it in client code.

  4. Send the registration request below with the URL, the events, and the secret.

  5. Read webhook_id from the 201 response and store it.

  6. Send a test event to that webhook_id.

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

name

string

Yes

—

Your label for the webhook.

url

string

Yes

—

The HTTPS URL that receives the POST.

events

array of strings

Yes

—

One or more of job.completed, job.failed, quota.exceeded.

secret

string

No

none

The HMAC signing secret. Without a secret, RichAPI sends no signature header.

api_slugs

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

X-RichAPI-Signature

Algorithm

HMAC-SHA256

Key

The secret of the webhook, as UTF-8 bytes

Signed data

The raw request body, byte for byte

Header format

sha256= followed by the lower-case hexadecimal digest

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?

  1. Read the raw request body as bytes, before any JSON parse.

  2. Read the X-RichAPI-Signature header. Treat a missing header as a failure.

  3. Compute sha256= plus the HMAC-SHA256 hexadecimal digest of the raw body.

  4. Compare the two strings with a constant-time comparison.

  5. On a mismatch, return 401 and 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 200 to 299

Time limit for the response

10 seconds per attempt

Redirect responses

Not followed. A 301 or a 302 counts as a failure.

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

https only. An http URL is rejected.

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?

  1. Register the webhook and store the webhook_id.

  2. Send the test request below.

  3. Read the payload object in the response. That object is the exact body sent.

  4. Read the delivery log and check the status of the newest record.

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

status

string

No

all

Filters on delivered or failed. Any other value returns 400.

event

string

No

all

Filters on one event name.

start_date

string

No

none

ISO 8601 time. Returns records at or after this time.

end_date

string

No

none

ISO 8601 time. Returns records at or before this time.

limit

integer

No

50

Records per page. The maximum is 100.

cursor

string

No

none

The next_cursor value from the previous response.

The response holds the records, newest first, and a cursor when more records exist.

Field

Type

Description

delivery_id

string

The identifier of this attempt. Use it for a redelivery.

webhook_id

string

The webhook that made the attempt.

event

string

The event name of the payload.

status

string

delivered or failed.

http_code

integer

The status your server returned. The value is null after a network error or a blocked URL.

latency_ms

integer

The round-trip time in milliseconds.

attempt

integer

1 for the first try, 3 for the last retry.

payload

string

The exact JSON body that RichAPI sent, as a string.

created_at

number

The time of the attempt, as a Unix timestamp.

expires_at

string

The time RichAPI removes the record.

api_key_id

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

401 with API key required

The key is absent, wrong, or revoked.

Send the current key in the x-api-key header.

400 with url must be HTTPS and point to a public address

The URL is not HTTPS, or the hostname resolves to a private address.

Register a public HTTPS URL.

400 with url and name are required

The request body carries no url or no name.

Send both fields.

400 with events must be a non-empty list

The events field is absent or empty.

Send a list with at least one event name.

400 with Unsupported events

An event name is outside the three supported names.

Send only job.completed, job.failed, or quota.exceeded.

400 with Invalid JSON body

The body is not valid JSON.

Correct the JSON and send the request again.

400 with status must be 'delivered' or 'failed'

The status filter carries another value.

Use delivered or failed.

400 with Invalid cursor

The cursor value is altered or truncated.

Send the next_cursor value exactly as returned.

404 with Webhook not found

The identifier is wrong, or the webhook belongs to another API key.

List the webhooks with the same key and copy the identifier.

404 with Delivery not found

The delivery identifier is wrong or expired.

Read the delivery log and copy a current delivery_id.

405 with Method not allowed

The request used a method that the route does not accept.

Use GET, POST, or DELETE only.

A delivery is failed with http_code: null

A timeout, a network error, or a blocked URL.

Check public DNS, the TLS certificate, and the 10-second limit.

A delivery is failed with http_code: 301 or 302

Your endpoint redirects the POST.

Register the final URL, then delete the old webhook.

A delivery is failed with http_code: 500

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

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.

Related articles

Did this answer your question?