Webhooks
Webhooks notify your application in real-time when events happen — payments, subscription changes, refunds, and more. Instead of polling for a payment’s status, you register a URL and Recurrente makes a POST to it whenever something happens.
See the complete event catalog for when each webhook fires, its schema, and an example payload.
Integrating takes four steps: register your endpoint, handle the event, verify the signature, and test it.
Connected accounts: verification and FEL
verification.completed: the account verification was accepted.verification.rejected: one or more documents were rejected; inspectdocument_errors.tax_invoice_settings.enabled: FEL activation finished and invoicing is ready.tax_invoicingcontains the public state, without provider credentials.
The platform receives these events with connected: true and the child account’s account_id. Acceptance and activation payloads preserve the state at the transition; query GET /connected_accounts/{id}/tax_invoicing for the current FEL state. Saving data without changing status does not emit another event. See the event catalog for schemas and examples.
New accounts and compatibility
Accounts created after this change is enabled receive only intent.* when a unified replacement exists: card, bank transfer, crypto, balance, and cash. Existing accounts keep both formats, even when registering a new endpoint. Events without a replacement, such as automated_bank_transfer_intent.*, setup_intent.*, subscription.*, and refund.*, are unchanged; creation or update events without a unified equivalent are also preserved.
The rule follows the account receiving the webhook: an existing platform keeps both formats for its new connected accounts. Each new Sandbox uses the unified format even if its LIVE account predates the change.
The unified payload includes legacy_id, the same payment’s ID in the legacy per-type event. Use it to match historical records during migration; the canonical ID remains id.
1. Register your endpoint
An endpoint is a public URL on your server where you want to receive notifications (e.g. https://yoursite.com/webhooks/recurrente). Private, local, and metadata addresses are rejected.
Via Dashboard
Within your Recurrente account, go to Settings → Developers and API, click “Webhooks”, and add the URL where you want the requests to be sent.
Via API
You can register several URLs (production, staging, logging); each event is sent to all of them.
2. Handle the event
Each webhook delivers a JSON event with this structure. Your handler branches on the type field to know which payment method generated it, and on status to know what happened.
The payload has three layers:
Envelope — the event’s identifying fields:
type— the payment type:payment,bank_transfer,crypto,balance,cash.event_type— the event:intent.succeeded,intent.failed,intent.pending,intent.canceled,intent.paid.status— the state:pending,succeeded,failed,canceled,paid. Some intermediate states (e.g. card) are exposed aspending; the processor’s exact value is always inraw_status.raw_status,id,legacy_id,receipt_number,api_version,created_at.
Payment data — amount_in_cents, currency, customer, product, tax_invoice_url, checkout, payment.
details — fields specific to the payment method. For payment: fees, installments, channel, etc. For bank_transfer: bank_reference (the reference assigned by the sender’s bank or the payment network) and sender_comment (the free-text comment the payer wrote on the transfer).
Example (intent.succeeded, card payment):
The envelope and payment data are the same for all types; only type and the details contents change. So a single handler covers every payment method:
You can also fetch any payment via API with GET /api/intents/{id} — it returns exactly the same format as the webhook.
3. Verify the signature
Every webhook includes a signature so you can verify it came from Recurrente and wasn’t tampered with. We strongly recommend verifying signatures in production.
Signing Secret
Each endpoint has a unique signing secret, formatted as whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw. There are two ways to get it:
- Via API: the
signingSecretfield is returned in the response when you create the endpoint (POST /api/webhook_endpoints). It is only returned in that creation response, so store it securely. - Via dashboard: you can find it at any time in the Svix dashboard — accessible from Settings → Developers and API → Webhooks in your Recurrente account.
Verifying with Official Libraries (Recommended)
The easiest way to verify signatures is using the Svix webhook verification libraries, available for many languages:
You must use the raw request body when verifying webhooks. Parsing the JSON and re-serializing it will break the signature verification.
Verifying Manually
Each webhook request includes three headers:
Step 1. Construct the signed content by concatenating the svix-id, svix-timestamp, and the raw request body, separated by dots:
Step 2. Decode the base64 portion of your signing secret (everything after whsec_) and use it as the key for an HMAC-SHA256 hash of the signed content:
Step 3. Compare your computed signature with the one in the svix-signature header. The header value is prefixed with v1, — strip that prefix before comparing. Use a constant-time comparison to prevent timing attacks.
Step 4. Optionally, verify that the svix-timestamp is within a few minutes of your server’s current time to prevent replay attacks.
4. Test it
A Sandbox delivers webhooks so you can build your integration without moving real money. Paying a Sandbox checkout with the 4242 4242 4242 4242 card fires intent.succeeded (type: payment). Sandboxes predating the change also keep payment_intent.succeeded. Events are delivered only to endpoints registered with that Sandbox’s TEST key (sk_test_*). Test payments only simulate cards.
You can also open the webhook dashboard and use the Testing tab to send your endpoint an example of any event in the catalog. The resulting message shows the payload, delivery attempts, and whether delivery succeeded. Test payments only simulate cards; use the corresponding environment to exercise the real processing flow for other payment methods.
Event types
Payment events (intent.*)
A single balance charge triggers two independent webhooks: intent.succeeded (type: balance) to the merchant account and intent.paid to the payer account.
Subscription events
Note: in a subscription, when a payment fails, Recurrente attempts to charge it again 3 and 5 days later. If both retry attempts fail, the subscription is canceled at that time.
Other events
Money-movement events
withdrawal.* and swap.* events include a transfer key carrying the same unified shape GET /api/transfers/{id} returns (transfer.* events already use it as their main shape). If you integrate against the unified endpoint, always read that key — see the Move money guide.
For payouts, read payload.withdrawal.status (or payload.transfer.status in the unified shape). A withdrawal.update with status: "sent" is terminal success for local bank payouts: run your next step and do not wait for a later completed. If completed arrives, treat it only as an additional confirmation.
Retries
If your endpoint doesn’t respond with a 2xx code, Recurrente will retry sending the webhook:
- Immediately after the first failure
- 1 minute later
- 5 minutes later
- 30 minutes later
- 2 hours later
- 6 hours later
Best Practices
- Return 2xx quickly — Acknowledge receipt before doing heavy processing
- Handle duplicates — Webhooks may be delivered more than once; use the event ID for idempotency
- Verify the source — Validate webhook signatures as described above
- Process asynchronously — Use a queue for heavy processing after acknowledging receipt
Does your account receive per-type events (payment_intent.*, bank_transfer_intent.*, etc.)? They keep working unchanged — see Legacy events or the guide to migrate to the unified format.
P2P transfers can remain in_review. The amount is reserved until approval. Rejection changes the status to cancelled and returns the amount to the sender.

