Webhooks
Server-to-server event notifications · Signature verification · Checkout & payment lifecycle · Voids & refunds
Webhooks
RapidCents sends server-to-server POST notifications when checkout sessions and payments change. Treat verified webhooks as the source of truth for settlement, voids, and refunds — return URLs and iframe messages are UX signals only.
Complements checkout guides. Session creation and payment UI are covered in the checkout overview. This page covers receiving and processing outbound events.
Endpoint & Headers
Configure RapidCents to POST JSON to your HTTPS endpoint. In this demo app:
POST {APP_URL}/webhooks/payment
Replace {APP_URL} with your public base URL (must match the active gateway environment).
| Header | Required | Notes |
|---|---|---|
Content-Type | Yes | Must be application/json |
Signature or X-Signature | When secret set | HMAC-SHA256 of the raw body |
X-Gateway-Environment-Id | Optional | Identifies the originating gateway environment |
Respond with HTTP 2xx and a small JSON body (for example {"ok": true}) promptly. Defer slow work to a queue if needed.
Signature Validation
When webhook_secret is stored on the gateway environment, the demo app requires a valid signature. Algorithm: hash_hmac('sha256', raw_body, webhook_secret) compared with constant-time equality to the header value.
expected = HMAC_SHA256(raw_request_body, webhook_secret)
provided = request.header("Signature") ?? request.header("X-Signature")
if webhook_secret is set and not secure_compare(expected, provided):
return 401
Always compute the HMAC over the raw request body bytes, before any JSON parsing or re-serialization. Re-encoding the payload will change the signature and cause valid webhooks to be rejected.
Envelope Structure
Current RapidCents outbound webhooks use an envelope with eventType, identifiers, and a nested payload object. Event types are prefixed with rapidcents. (for example rapidcents.payment.succeeded); handlers typically strip that prefix.
| Field | Type | Description |
|---|---|---|
eventType | string | Event name, prefixed with rapidcents. |
webhookId | string | Unique webhook delivery id (primary dedup key) |
notificationId | string | Notification id (dedup fallback) |
eventDate | string | ISO-8601 timestamp of the event |
payload | object | Nested event data (see inner payload fields) |
{
"eventType": "rapidcents.payment.succeeded",
"webhookId": "wh_01HXABCDEF",
"notificationId": "ntf_01HXABCDEF",
"eventDate": "2026-05-22T14:30:00Z",
"payload": {
"sourceName": "checkout_session",
"sourceId": "550e8400-e29b-41d4-a716-446655440000",
"sessionToken": "cs_live_a1b2c3",
"sessionStatus": "completed",
"paymentStatus": "paid",
"amountTotal": 49.99,
"currency": "USD",
"metadata": {
"local_checkout_session_id": "164",
"order_id": "42"
},
"transaction": {
"id": "txn_987654",
"originalTransactionId": "txn_987654",
"authAmount": 49.99
}
}
}
Event Types
Checkout-related events handled by this demo app (suffix after rapidcents.):
checkout.session.created
Session opened at gateway
checkout.session.expired
Session timed out
checkout.session.cancelled
Shopper or merchant cancelled
payment.succeeded
Charge approved
payment.failed
Decline or error
payment.voided
Authorization voided
payment.refunded
Full refund
payment.partially_refunded
Partial refund
Payment events with sourceName other than checkout_session are ignored by this demo. Legacy suffixes such as checkout.payment.succeeded are normalized to payment.succeeded.
Inner Payload Fields
Use these fields to correlate a webhook with your local checkout session:
| Field | Description |
|---|---|
sourceName | Should be checkout_session for checkout integrations. |
sourceId | Gateway checkout session UUID — primary correlation key. |
sessionToken | Public session token (same as create/pay response token). |
sessionStatus | Gateway session lifecycle: pending, completed, expired, cancelled, failed. |
paymentStatus | Gateway payment state: unpaid, paid, failed, etc. |
metadata.local_checkout_session_id | Optional id you sent at session create — fastest lookup in this app. |
transaction.id | Charge / transaction id for void, refund, and reconciliation. |
transaction.authAmount | Amount for refund webhooks (preferred over amountTotal). |
isFinalFailure | On payment.failed, when true marks terminal failure. |
Sample Payloads
payment.succeeded
{
"sourceName": "checkout_session",
"sourceId": "550e8400-e29b-41d4-a716-446655440000",
"sessionToken": "cs_live_a1b2c3",
"sessionStatus": "completed",
"paymentStatus": "paid",
"amountTotal": 29.99,
"currency": "USD",
"metadata": { "local_checkout_session_id": "164" },
"transaction": {
"id": "txn_987654",
"originalTransactionId": "txn_987654",
"authAmount": 29.99
}
}
payment.voided
{
"sourceName": "checkout_session",
"sourceId": "550e8400-e29b-41d4-a716-446655440000",
"sessionToken": "cs_live_a1b2c3",
"sessionStatus": "cancelled",
"paymentStatus": "voided",
"transaction": {
"id": "txn_987654",
"originalTransactionId": "txn_987654"
}
}
payment.partially_refunded
{
"sourceName": "checkout_session",
"sourceId": "550e8400-e29b-41d4-a716-446655440000",
"sessionToken": "cs_live_a1b2c3",
"sessionStatus": "completed",
"paymentStatus": "partially_refunded",
"amountTotal": 29.99,
"transaction": {
"id": "txn_987654",
"originalTransactionId": "txn_987654",
"authAmount": 5.00
}
}
authAmount on the transaction object is the refunded amount for this event. Full refunds use payment.refunded with the same shape.
payment.failed
{
"sourceName": "checkout_session",
"sessionToken": "cs_live_a1b2c3",
"sessionStatus": "pending",
"paymentStatus": "failed",
"isFinalFailure": false
}
Void & Refund
Void and refund are initiated through RapidCents transaction APIs (from your server or this app’s admin UI). Status changes are applied when the matching webhook arrives, not when the API returns.
| Action | Request | Awaited Webhook |
|---|---|---|
| Void | POST {API origin}/api/{business_id}/transactions/{transaction_id}/void |
payment.voided |
| Refund | POST {API origin}/api/{business_id}/transactions/{transaction_id}/refund with amount |
payment.refunded or payment.partially_refunded |
Admin UI in this demo. Admin → Checkout sessions → Void / Refund submits the API call and shows a pending state until the webhook updates payment_status and refund history.
This Demo App (https://rapidcheckout.rapidcents.net/)
| Concern | Implementation |
|---|---|
| Entry point | WebhookController → WebhookProcessingService. |
| Dedup | payment_webhooks.event_id stores webhookId (fallback notificationId). |
| Lookup order | metadata.local_checkout_session_id, stored gateway_source_id, external_session_id / token. |
| Columns | checkout_sessions.status (session lifecycle) and payment_status (paid, voided, refunded, etc.) are synced from webhook payloads. |
Handler Checklist
- Verify — Read the raw body; verify HMAC when
webhook_secretis configured. - Parse & dedupe — Parse the JSON envelope; dedupe by
webhookId. - Branch — Branch on
eventType(strip therapidcents.prefix). - Resolve — Resolve the local session using
sourceId,sessionToken, ormetadata. - Update — Update order / payment state idempotently.
- Respond — Return 2xx quickly; log unmatched events for investigation.
Idempotency matters. Webhooks may be retried or delivered out of order. Always make state transitions safe to apply more than once and never downgrade a terminal state (e.g., do not move a refunded payment back to paid).