Docs
Checking Raims account
IntegrationsWebhooks

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"
  }
}
FieldMeaning
idEvent UUID, preserved in redeliveries and across destinations receiving the same event.
typeEvent name, such as charge.paid, refund.completed, or webhook.test.
created_atEvent date in UTC, in ISO 8601 format. It does not represent the time of each attempt.
environmentsandbox or production, according to the resource's environment.
data.objectResource type: charge for a charge, refund for a refund, or webhook for a test.
data.idPublic 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

HeaderMeaning
webhook-idDelivery UUID for that destination. It remains the same in retries.
webhook-timestampAttempt time in Unix seconds in UTC.
webhook-signatureOne 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

  1. Capture the HTTP body as bytes before parsing the JSON.
  2. Read the three signature headers.
  3. Remove the whsec_ prefix from the registration's secret and decode the remaining part as Base62, in this order: digits from 0 to 9, letters from A to Z, and letters from a to z. Represent the number in 32 little-endian bytes, padding with zeros on the right when needed. These bytes are the HMAC key.
  4. Build the message from the UTF-8 bytes of webhook-id + "." + webhook-timestamp + ".", followed by the exact bytes of the received body.
  5. Split the header's signatures by comma and select the v1 values. 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:

  1. Validate the signature using the original body and check the event environment.
  2. Durably record the event id and the work to be processed. Ensure this acceptance prevents duplicates even when two requests arrive at the same time.
  3. If the event has already been accepted, return 2xx without recording the same work again. Otherwise, acknowledge receipt after saving.
  4. 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.