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.
1. Register your endpoint
An endpoint is a URL on your server where you want to receive notifications (e.g. https://yoursite.com/webhooks/recurrente).
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,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
Test mode delivers simulated webhooks so you can build your integration without moving real money. Paying a test checkout with the 4242 4242 4242 4242 card fires payment_intent.succeeded and intent.succeeded (type: payment) only to endpoints registered with your test keys (pk_test_* / sk_test_*). Test payments only simulate cards.
Because test payments are approved directly instead of going through the full processing flow, the payload is simulated from the checkout: the intent id carries a pa_test_ prefix and there is no linked payment object.
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.
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
Integrated before June 2026 with the 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.

