Skip to main content
Revtain sends webhook notifications to the URL you registered during onboarding when a recovery attempt reaches a final state. Every webhook is signed for verification.

Webhook Security

Every outbound webhook includes an X-Revtain-Signature header containing an HMAC-SHA256 signature computed using your webhookSigningSecret.
Always verify signatures before processing webhook events. Unsigned or invalid requests should return 401 Unauthorized.

Step-by-Step Verification

1

Set up your webhook endpoint

HTTPS endpoint that accepts POST with JSON body and returns 200 OK.
2

Store your webhook signing secret

Save webhookSigningSecret securely. This is different from your API key.
3

Verify every incoming request

Compute HMAC-SHA256 using your secret, compare with X-Revtain-Signature.
4

Process the event and respond 200

Always respond within 10 seconds. Slow endpoints trigger retries.

Code Examples

Sign against the raw request body, exactly as received — not a re-serialized copy. The signature is computed over the precise bytes Revtain sent. If you let your framework parse the JSON and then re-stringify it (JSON.stringify(req.body)), key ordering or whitespace can differ and every signature check will fail. Capture the raw body first, verify, then parse.
Always respond with 200 within 10 seconds. If your endpoint is down or slow, Revtain retries delivery automatically — three immediate attempts (0s, 2s, 8s), then, if those fail, a durable retry schedule over the following 24 hours. A brief outage on your side will not lose an event.

Event Reference

recovery.success

Sent when a previously failed payment has been successfully recovered.
{
  "event": "recovery.success",
  "revtainTransactionId": "da646dba-ce56-4483-ad0a-2a4fac54a5e2",
  "gatewayTransactionToken": "pi_3OZQ8K2eZvKYlo2C1abc1234",
  "amount": 5000,
  "currency": "USD",
  "message": "Succeeded!",
  "strategyUsed": "primary"
}
Action: Mark the customer’s invoice as paid in your billing system.
Correlating a webhook back to your invoice. When you call /api/recovery/execute, the response includes a transactionId. Store it against your invoice. The webhook’s revtainTransactionId is the same value — match on it to know which invoice this outcome belongs to. If you prefer to drive everything from your own key, pass an idempotencyKey (your invoice ID) on /execute and poll GET /api/recovery/status?idempotencyKey=... instead of waiting on the webhook.
recovery.success and recovery.failed may carry an optional trigger field identifying what produced the outcome — for example a scheduled retry, or "reconciliation" when Revtain’s automatic reconciliation resolved a payment whose original gateway response was lost in transit. Handle both events the same way regardless of trigger; the field exists for your telemetry.

recovery.failed

Sent when all retry strategies have been exhausted.
{
  "event": "recovery.failed",
  "revtainTransactionId": "da646dba-ce56-4483-ad0a-2a4fac54a5e2",
  "gatewayTransactionToken": "FAILED_API_1773792234011",
  "amount": 5000,
  "currency": "USD",
  "message": "Card declined",
  "strategyUsed": "all_failed",
  "recommendedAction": "retry_later",
  "recommendedActionReason": "The issuer returned a soft decline. A later retry, or a card update, may succeed."
}
Action: Branch on recommendedAction (see table below) and run your own customer outreach from your domain — see the dunning guide. Revtain never emails or messages your customers directly.

recommendedAction values

recovery.failed and recovery.blocked both carry a recommendedAction field, so your dunning logic can branch on a stable signal instead of parsing raw decline codes. recommendedActionReason is a human-readable explanation of the same signal.
ValueWhat to do
retry_laterA soft or transient decline. A later retry — or a card update — may succeed.
request_card_updateThe card is expired, invalid, or needs customer authentication. Ask the customer to update or re-confirm their payment method.
manual_reviewThe issuer flagged the card for fraud. Do not retry — review the account.
monitorThe reason is ambiguous (for example a suspected duplicate). Verify before retrying.

recovery.blocked

Sent when a transaction is stopped before any retry — either by the Risk Engine (fraud, lost card) or because the issuer requires the customer to authenticate.
{
  "event": "recovery.blocked",
  "declineCode": "fraudulent",
  "reason": "Transaction blocked by Revtain Risk Engine to protect Merchant Health.",
  "recommendedAction": "manual_review",
  "recommendedActionReason": "The issuer flagged this card for fraud. Do not retry — review the account.",
  "paymentMethodToken": "pm_1234567890",
  "amount": 5000,
  "currency": "USD",
  "timestamp": "2026-03-17T23:15:21.000Z"
}
Action: Branch on recommendedAction. A manual_review value means flag the account and do not retry. A request_card_update value — for example when declineCode is authentication_required — means route the customer through your checkout to re-confirm their card.

card.updated

Sent when a customer updates their payment method via a hosted card update link.
{
  "event": "card.updated",
  "oldPaymentMethodToken": "pm_oldcard123",
  "newPaymentMethodToken": "pm_newcard456",
  "paymentMethodType": "apple_pay",
  "eligibleGateways": ["stripe", "adyen"],
  "clientId": "da646dba-ce56-4483-ad0a-2a4fac54a5e2",
  "timestamp": "2026-04-20T14:30:00.000Z"
}
FieldTypeDescription
oldPaymentMethodTokenstringThe token that was dead and triggered the update flow. Stop using it.
newPaymentMethodTokenstringThe new gateway-native token. Use this on all future recovery calls.
paymentMethodTypestringcard, apple_pay, google_pay, paypal, or venmo — what the customer chose on the update page. Pass this back as paymentMethodType on future /api/recovery/execute calls so the engine applies the right strategy.
eligibleGatewaysstring[]The list of your configured gateways the new token can be charged on. For card tokens this is usually every gateway; for wallet tokens it’s gateway-locked (the wallet sheet binds the token to whichever gateway received it).
clientIdstringYour Revtain client ID — same value Revtain assigned at onboarding.
timestampstringISO 8601 timestamp of the update event.
Action: Store newPaymentMethodToken and paymentMethodType against the customer record in your system. Use them on all future POST /api/recovery/execute calls for this customer. The old token is no longer valid for recovery — discard it.
newPaymentMethodToken is a gateway-native token (for example a Stripe pm_xxx or a Checkout.com src_xxx), issued by the same gateway that holds the customer’s account. You can pass it to your gateway directly as well — it’s a real gateway token, not a Revtain reference.

predict.risk.high

Sent when the pre-failure risk prediction engine detects a high-risk payment before it fails. Enables proactive intervention.
{
  "event": "predict.risk.high",
  "paymentMethodToken": "pm_1234567890",
  "riskScore": 82.5,
  "reasoning": "Card has 3 prior declines in last 30 days. High-risk pattern detected.",
  "amount": 5000,
  "timestamp": "2026-04-20T14:30:00.000Z"
}
Action: Consider reaching out to the customer to update their payment method before the next billing cycle. Generate a card update link via /api/recovery/update-card/generate.

recovery.skipped_high_risk

Sent when the predictor scored the failure so low that attempting recovery would burn issuer velocity budget without a realistic chance of success. Revtain skips the retry entirely and surfaces a pre-generated card update link so you can route the customer straight to re-entering payment details.
{
  "event": "recovery.skipped_high_risk",
  "paymentMethodToken": "pm_1234567890",
  "amount": 5000,
  "currency": "USD",
  "riskScore": 92,
  "confidence": 88,
  "reasoning": "Card has 5 prior hard declines in the last 7 days and the predicted retry success probability is below the configured threshold.",
  "cardUpdateUrl": "https://pay.yourdomain.com/api/recovery/update-card/abc123xyz",
  "skippedChargeId": "skip_a1b2c3d4",
  "timestamp": "2026-04-20T14:30:00.000Z"
}
FieldTypeDescription
riskScorenumber0–100, where higher = more likely to fail. The configured skip threshold lives on your client record.
confidencenumber0–100 confidence in the prediction. Useful for filtering — a high score with low confidence is a weaker signal.
reasoningstringHuman-readable explanation of why this card was skipped.
cardUpdateUrlstringA ready-to-send card update link the customer can use to provide new credentials.
skippedChargeIdstringInternal ID for the skip event — quote this if you need to ask support about a specific decision.
Action: Send the cardUpdateUrl to the customer via your existing channel (email, in-app message). Treating a skip as “the recovery failed” lets you maintain a clean state machine in your dunning logic.
This event is additive — older integrations that don’t handle it can safely ignore it. The same skip also fires Revtain’s normal dunning hooks, so you don’t need to wire recovery.skipped_high_risk unless you want richer telemetry on why a particular charge was skipped.

recovery.holdout

Sent only on accounts running Proof Mode. The request landed in the measurement control group: Revtain made no recovery attempt and will not make one.
{
  "event": "recovery.holdout",
  "clientId": "da646dba-ce56-4483-ad0a-2a4fac54a5e2",
  "paymentMethodToken": "pm_1234567890",
  "amount": 4900,
  "currency": "USD",
  "customerEmail": "customer@example.com",
  "message": "Proof mode control group: Revtain made no recovery attempt for this payment. Proceed with your standard failed-payment process.",
  "timestamp": "2026-06-13T10:15:00.000Z"
}
Action: run your standard failed-payment process for this payment — exactly what you did before Revtain. If your integration doesn’t handle this event, nothing breaks: the synchronous API response for held-out requests reports success: false, so your existing failure path already does the right thing.

recovery.proactive_retention

Sent when Revtain detects that a single customer has hit multiple recovery failures in a short window — a strong signal they’re at risk of churning. The event carries a pre-generated cancel-flow URL you can offer the customer to retain them with a pause, discount, or downgrade option before they actively cancel.
{
  "event": "recovery.proactive_retention",
  "customerEmail": "customer@example.com",
  "recentFailureCount": 3,
  "cancelFlowUrl": "https://pay.yourdomain.com/api/cancel-flow/abc123xyz",
  "cancelFlowToken": "abc123xyz",
  "amount": 2900,
  "currency": "USD",
  "reason": "Customer has had repeated recovery failures. Send this link via your existing channel to offer retention options before they churn.",
  "timestamp": "2026-04-20T14:30:00.000Z"
}
FieldTypeDescription
customerEmailstringThe customer whose failure pattern triggered the proactive retention flag.
recentFailureCountnumberHow many failed recoveries this customer has had in the recent window.
cancelFlowUrlstringA hosted cancel-flow URL the customer can use to pause, downgrade, or cancel — your retention surface for at-risk customers.
cancelFlowTokenstringThe raw token (same one embedded in cancelFlowUrl) so you can build your own page if you’d rather host it yourself.
reasonstringPlain-language explanation of why this was triggered — safe to surface in internal dashboards.
Action: Send cancelFlowUrl to the customer via your existing channel. Customers who reach this link voluntarily are dramatically more likely to retain (via pause / downgrade) than customers who are forced through the dunning gauntlet first. Requires the cancel-flow feature to be enabled on your account.
recovery.proactive_retention is only fired for clients with the cancel-flow feature enabled. If you don’t use cancel flow today, this event never fires — you can safely ignore it.

card.expiring_soon

Sent ahead of a renewal when the customer’s card on file is close to its expiry date, so you can prompt the customer before the payment fails. Fired during a pre-renewal cascade for clients on DIRECT_GATEWAY mode (Stripe / Checkout.com).
{
  "event": "card.expiring_soon",
  "clientId": "da646dba-ce56-4483-ad0a-2a4fac54a5e2",
  "paymentMethodToken": "pm_1234567890",
  "expiryMonth": 8,
  "expiryYear": 2026,
  "message": "Card expires 8/2026 — consider prompting customer to update."
}
Action: Reach out to the customer and ask them to update their card before the next renewal.
Field names are expiryMonth and expiryYear (not expMonth/expYear). Treat any additional fields as optional and forward-compatible.

Pre-emptive variant (card health sweep)

Accounts with the card health sweep enabled also receive card.expiring_soon from Revtain’s own monitoring — a routine background check of active cards, independent of any billing platform. This variant is marked preemptive: true and includes a ready-made card-update link:
{
  "event": "card.expiring_soon",
  "clientId": "da646dba-ce56-4483-ad0a-2a4fac54a5e2",
  "paymentMethodToken": "pm_1234567890",
  "customerEmail": "customer@example.com",
  "expiryMonth": 7,
  "expiryYear": 2026,
  "cardUpdateUrl": "https://pay.yourdomain.com/update-card/abc123xyz",
  "preemptive": true,
  "message": "Card expires 7/2026. Send the customer the card update link before the next billing attempt.",
  "timestamp": "2026-06-13T04:00:00.000Z"
}
Action: forward cardUpdateUrl to the customer through your own channel. The link stays valid for 14 days. Updating the card before the renewal means the decline never happens — the cheapest recovery there is.
Sent after a failed recovery when the decline pattern suggests the charge would clear with 3-D Secure authentication — typically a soft decline from a European or UK issuer.
{
  "event": "recovery.3ds_recommended",
  "paymentMethodToken": "pm_1234567890",
  "amount": 5000,
  "currency": "USD",
  "declineCode": "do_not_honor",
  "reason": "Soft decline from EU/UK issuer may benefit from 3D Secure authentication. Re-attempt this charge with 3DS enabled on your checkout."
}
Action: Re-attempt the charge through your own checkout with 3-D Secure enabled, so the customer can authenticate.

churn.flow.{outcome}

Sent when a customer completes the hosted cancel flow. The event name carries the outcome: churn.flow.retained, churn.flow.paused, churn.flow.downgraded, or churn.flow.cancelled.
{
  "event": "churn.flow.paused",
  "customerId": "cus_abc123",
  "customerEmail": "customer@example.com",
  "subscriptionId": "sub_789",
  "outcome": "paused",
  "timestamp": "2026-05-21T16:40:00.000Z"
}
Action: Apply the customer’s decision in your billing system — pause, downgrade, or cancel the subscription, or take no action if they were retained.