Receive and validate webhooks
Validate webhook signatures and process notifications safely, including redeliveries.
Prepare an HTTPS endpoint that accepts POST with
Content-Type: application/json. Use the default port 443 and a domain
whose DNS resolves to a public IPv4 address. It must receive calls between
systems without interactive login, CAPTCHA, or redirects. Configure the
destination and events in Webhooks, and allow the
five official IPs when sources are restricted.
Event body
Each request identifies the event, environment, and changed resource:
{
"id": "d377b4c5-5678-4c42-8ea3-8f823156948e",
"type": "charge.paid",
"created_at": "2026-10-15T12:30:00.000Z",
"environment": "sandbox",
"data": {
"object": "charge",
"id": "ch_4ZxYp8Q2mV7nR6sT5uW3aB"
}
}| Field | Meaning |
|---|---|
id | Event UUID, preserved in redeliveries and across destinations receiving the same event. |
type | Event name, such as charge.paid, refund.completed, or webhook.test. |
created_at | Event date in UTC, in ISO 8601 format. It does not represent the time of each attempt. |
environment | sandbox or production, according to the resource's environment. |
data.object | Resource type: charge for a charge, refund for a refund, or webhook for a test. |
data.id | Public resource identifier: charge ch_, refund rf_, or webhook whk_. |
The body keeps the same bytes in every attempt. Use data.id to query the
current state in Charges or
Refunds with a key from the same company and
environment. The event reports a change; processing it must not assume this
is still the resource's latest state.
data identifies the resource and does not contain the complete charge or
refund record. created_at uses ISO 8601 in this body; dates returned by
delivery and attempt queries use Unix seconds in UTC.
Signature headers
| Header | Meaning |
|---|---|
webhook-id | Delivery UUID for that destination. It remains the same in retries. |
webhook-timestamp | Attempt time in Unix seconds in UTC. |
webhook-signature | One or more v1= signatures, each with 64 hexadecimal characters, separated by commas. |
The webhook-id differs from the body's id: an event may be delivered to
more than one destination or manually resent. Each delivery has its own
webhook-id. Retries of the same delivery preserve that ID and the original
body but may have a different timestamp and signature.
During a transition period chosen through the API,
the same header contains the new secret's signature first, followed by the
previous secret's: v1=<assinatura nova>,v1=<assinatura anterior>. The
receiver must accept the request when any v1 signature matches the
configured secret. When the deadline passes or an immediate rotation ends
the transition, new sends include only the current secret's signature.
Validate the signature
- Capture the HTTP body as bytes before parsing the JSON.
- Read the three signature headers.
- Remove the
whsec_prefix from the registration's secret and decode the remaining part as Base62, in this order: digits from0to9, letters fromAtoZ, and letters fromatoz. Represent the number in 32 little-endian bytes, padding with zeros on the right when needed. These bytes are the HMAC key. - Build the message from the UTF-8 bytes of
webhook-id + "." + webhook-timestamp + ".", followed by the exact bytes of the received body. - Split the header's signatures by comma and select the
v1values. Calculate HMAC-SHA256 and compare the result with each signature using a constant-time comparison. Accept the request if one matches.
Do not use the whsec_... text directly as the HMAC key. Do not serialize
the JSON again: spaces, line breaks, and property order are part of the
signature.
Configure the framework to preserve the original body on this route. An object already converted from JSON to the application's language cannot be used to check the signature. Reject the request without applying its effects if the signature or timestamp is invalid.
We recommend accepting timestamps within five minutes of the receiver's clock and keeping that clock synchronized. This check reduces the reuse of old requests. Delivery deduplication remains necessary within that window.
The tolerance applies to the attempt's webhook-timestamp, not to the body's
created_at. Retries and resends receive a current timestamp and signature,
while the original event date remains in the body.
Node.js example
The example receives the private secret, headers, and a Buffer containing
the original body. It accepts either a single v1 or the transition period's
signatures, regardless of the valid signature's position. It does not log
these values or parse the JSON before validation.
import { createHmac, timingSafeEqual } from 'node:crypto';
const alphabet =
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
function decodeWebhookSecret(secret) {
if (!/^whsec_[0-9A-Za-z]{1,43}$/.test(secret)) {
throw new Error('Invalid webhook secret');
}
const encoded = secret.slice('whsec_'.length);
if (encoded.length > 1 && encoded.startsWith('0')) {
throw new Error('Invalid webhook secret');
}
let value = 0n;
for (const character of encoded) {
value = value * 62n + BigInt(alphabet.indexOf(character));
}
const key = Buffer.alloc(32);
for (let index = 0; index < key.length; index += 1) {
key[index] = Number(value & 255n);
value >>= 8n;
}
if (value !== 0n) throw new Error('Invalid webhook secret');
return key;
}
export function verifyWebhook(
secret,
headers,
rawBody,
nowSeconds = Date.now() / 1000,
) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signature = headers['webhook-signature'];
if (
!Buffer.isBuffer(rawBody) ||
typeof id !== 'string' ||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
id,
) ||
typeof timestamp !== 'string' ||
!/^\d{1,12}$/.test(timestamp) ||
!Number.isFinite(nowSeconds) ||
Math.abs(nowSeconds - Number(timestamp)) > 300 ||
typeof signature !== 'string'
) {
return false;
}
const signatures = signature
.split(',')
.map((entry) => entry.trim())
.filter((entry) => /^v1=[0-9a-f]{64}$/i.test(entry));
if (signatures.length === 0) return false;
const key = decodeWebhookSecret(secret);
try {
const expected = createHmac('sha256', key)
.update(`${id}.${timestamp}.`, 'utf8')
.update(rawBody)
.digest();
return signatures.some((entry) => {
const received = Buffer.from(entry.slice('v1='.length), 'hex');
return timingSafeEqual(expected, received);
});
} finally {
key.fill(0);
}
}After a valid signature, parse the JSON, check the environment, and record the delivery before responding. Store the secret on the integration server with restricted access. Never include it in URLs, browser code, screenshots, or support messages.
Acknowledge receipt
Respond with an HTTP 2xx status only after accepting the event into durable
storage in your system. Send lengthy processing to a queue and run it
asynchronously. A successful response confirms that the receiver accepted
the delivery; it does not confirm a new financial transaction.
The receiver must follow this sequence:
- Validate the signature using the original body and check the event environment.
- Durably record the event
idand the work to be processed. Ensure this acceptance prevents duplicates even when two requests arrive at the same time. - If the event has already been accepted, return
2xxwithout recording the same work again. Otherwise, acknowledge receipt after saving. - Process the accepted work and track its completion separately. If that processing fails after the response, your system must recover it: Raims has already received the delivery acknowledgement.
If you cannot store the event, do not acknowledge it with 2xx. Subsequent
processing must also tolerate repetitions without duplicating their effects.
The timeout for each webhook HTTP send is 5 seconds in total. Validate
the signature, record the event safely, and return 2xx within that time,
without waiting for lengthy processing to finish. The up to 72 hours in
Production is the retry window; a request does not stay open for that period.
Raims does not follow redirects; register the final HTTPS URL directly. Configure the certificate with a valid public chain.
Each attempt's details in the logs may show the endpoint response body, limited to 8 KiB, and the destination IP used for that connection. Partially captured responses carry a notice. Content that was not recorded differs from a confirmed empty body; older records may lack this information. The body helps explain errors returned by the integration but must not contain secrets or unnecessary personal data. Use the diagnostic guide to relate the attempt result to the correction needed in the receiver.
Handle redeliveries and ordering
A delivery may arrive more than once, including when the receiver already
accepted the event but the response was lost. Record webhook-id to
recognize retries of the same delivery and use the body's id to prevent
a manual resend of the same event from repeating its effect. Acknowledge
previously accepted duplicates with 2xx as well.
Do not depend on arrival order. Events for different resources or retries
may arrive in another sequence. Query the resource through the API when you
need to decide based on its current state. If different destinations feed
the same routine, the body's id identifies the same event across them.
Manually resend events
The Dashboard allows you to request a resend for up to 15 days after the event was created, when the payload is available, the webhook is active, and the event type remains selected in the registration. The action requires Webhooks, View event logs, and Resend webhook events.
In the API, use Resend an event for the
same webhook as a previous delivery. To choose the destination from the
event, use Send an event to a webhook,
including when that destination does not yet have a delivery. Both paths
require an event no more than 15 days old, its body available, and an active
webhook that still selects that event type, belonging to the key's company
and environment. API queries and resends use the integration's authentication.
Technical webhook.test events cannot be resent; request another test.
To recover multiple events, submit a list of deliveries or use Resend selected in the Dashboard logs. Each item remains subject to the individual resend conditions. Check the accepted and rejected results before repeating a request.
A resend creates a delivery with a new webhook-id and preserves the original
body bytes, including id and created_at. It uses the webhook's current
URL and secret. If a secret transition is active, the same signature rules
as other sends apply.
The request does not cancel automatic retries for the previous delivery.
When the queue is paused, the new delivery also waits for it to resume,
and its own deadline continues counting down. Prepare the receiver to
recognize the same body id across different deliveries. Requesting a
resend does not repeat the financial action that generated the event or
confirm receipt by the endpoint.
The retry window for this delivery starts when the resend request is
accepted: up to 72 hours in Production or up to four hours in Sandbox,
with a maximum of three retries after the initial attempt in Sandbox.
In Production, intervals increase exponentially and vary between attempts,
without a fixed count within the deadline. The limit for requesting another
resend remains fifteen days after the original event was created; resending
does not renew that limit or change the body's created_at.
Pause and advance attempts
Before maintenance, pause the webhook queue. Pending deliveries and newly selected events wait for the queue to resume within their original deadlines. Pausing does not interrupt an already authorized HTTP call or freeze the 72-hour Production or four-hour Sandbox window.
You can request a technical test while the queue is paused. After checking the receiver, resume the queue to allow sends that remain valid. Resuming does not recover expired deliveries.
When the queue is released and a delivery is waiting for a retry, use Advance an attempt if the receiver has already been fixed. This action keeps the same delivery and original limits. Acceptance of the request does not confirm that the send occurred; check its progress. To create another delivery for an eligible event, use manual resend.
Webhook failures and retries
An HTTP 2xx response completes the delivery. Connection failures, timeouts,
and other HTTP codes allow retries within the environment's limits. This
includes 3xx, 400, 401, 403, 404, 429, and 5xx. Raims does not
follow redirects: configure the final HTTPS URL directly.
The technical webhook.test event has one HTTP attempt and a deadline of
up to five minutes to be sent. The retry rules below apply to charge and
refund events.
In Production, Raims retries delivery for up to 72 hours from the event. Intervals increase exponentially and vary between attempts, up to a one-hour limit. There is no fixed number of attempts within that deadline.
In Sandbox, there are up to three retries after the first attempt, spread over a few hours, within a window of up to four hours from the event. Use this environment to check how your receiver handles duplicates and failures.
Retry-After may specify a minimum wait in seconds or an HTTP date, capped
at one hour. The header does not extend the delivery window or increase the
number of Sandbox attempts. Check the address, signature, and firewall rules
when investigating a failure; do not depend on an exact time for the next attempt.
Failure alerts
Failures do not automatically pause the queue or disable the webhook. Each delivery continues following its retry policy and deadline. After a delivery exhausts its limits, new events continue generating their own deliveries while the registration is active.
Raims notifies you in the Dashboard when the fifth and tenth failures of
the same episode are confirmed. Each alert is sent once per episode. An
HTTP 2xx response ends that episode; new failures may start another.
Alerts do not stop retries or extend their deadlines.
Sending these alerts by email depends on the Webhook failure preference. Recipients are verified email addresses belonging to the account. These emails are grouped by company, environment, and recipient. The first send is immediate. If there are new notices, subsequent intervals are 2, 5, 10, 15, 20, 25, and 30 minutes, counted from the previous send. After that, the interval remains 30 minutes while notices continue arriving. After 30 minutes with no new notices and no summaries waiting to be sent, the next notice restarts the sequence, respecting the interval since the last send. Empty or periodic emails are not created merely because time has passed.
Each email presents up to twenty webhooks, the total count, and a link to view the rest in the Dashboard.
Individual notices remain available in the Dashboard. Check the current registration state and logs to investigate a failure; an email summary may include alerts from before the endpoint recovered. Confirmations of manual actions, such as editing or disabling a webhook, are not included in this grouping.
Fix the receiver and follow subsequent attempts. If you disabled the webhook manually, re-enable it to allow new sends. Re-enabling starts a new failure episode and preserves the registration, secret, and selected events. Rotating the secret does not reset the count or re-enable a disabled webhook. Editing the URL or events also preserves the registration state, failure count, and retry deadlines.
Disabling the registration prevents new sends. If a pending delivery's next attempt finds the webhook disabled, that delivery is closed. Re-enabling it before that attempt allows it to continue within the original limits. Re-enabling does not resend earlier events or automatically recover closed deliveries. When eligible, an event can receive a new delivery through manual resend. Requests in progress may still finish.
Test receipt through the API
After preparing the receiver, use Send a test event
for the active webhook. The event arrives at the same URL and uses the same
signature validation as other sends, with type: webhook.test,
data.object: webhook, and the registration identifier in data.id.
The test checks connectivity and signature validation. It does not change charges, payments, refunds, balance, or the webhook's failure count. Query the returned delivery to check the result. The record may take a few moments to appear.
The up-to-five-minute deadline includes waiting until the test can be sent.
The HTTP call to the receiver remains limited to five seconds. A 2xx
response to the test does not prove that your application handles financial
events; check that flow with Sandbox simulations.
Every new request creates a test. If it fails, fix the integration before requesting another; the technical event has no automatic retry after its HTTP attempt.
Test webhooks in Sandbox
Register a destination in Sandbox and select the events you want. Create a charge and use Simulation to confirm a payment or test failures and refunds. Confirmed changes generate the corresponding Sandbox events for that destination without moving real money.
Webhook requests are real and use the same official IPs as Production.
The body reports environment: sandbox. Keep the receiver and effects of
this test separate from your system's Production data.