Recurring Payment Webhooks
SaySwitch sends recurring lifecycle events to the webhook URL configured for the matching test or live environment. These events tell your server when a subscription changes state or a scheduled charge succeeds, fails, or needs customer action.
See the main Webhooks documentation for configuration and general delivery guidance.
Request Headers
Recurring webhooks use the same signing method as other SaySwitch webhooks.
| Header | Description |
|---|---|
payloadSignature | HMAC SHA512 signature of the raw JSON request body using your secret key. |
merchantSignature | HMAC SHA512 signature of your public key using your secret key. |
timestamp | ISO 8601 timestamp generated when the webhook is sent. |
Content-Type | application/json |
payloadSignature = hash_hmac("SHA512", raw_json_payload, merchant_secret_key)merchantSignature = hash_hmac("SHA512", merchant_public_key, merchant_secret_key)Use test keys to verify test webhooks and live keys to verify live webhooks. Calculate payloadSignature from the unmodified raw request body before parsing JSON, and compare signatures with a constant-time comparison.
Event Names
| Event | Meaning |
|---|---|
subscription.initialized | A subscription was created and is waiting for customer authorization. |
subscription.activated | Card authorization and consent succeeded, and the subscription became active. |
subscription.authorization_failed | The hosted card authorization attempt failed. |
subscription.authorization_expired | The customer did not complete authorization before the session expired. |
subscription.authentication_required | The customer must complete an authentication step for the initial charge. |
subscription.charge_succeeded | A billing invoice was charged successfully. |
subscription.charge_failed | A billing invoice charge failed. The subscription may become past_due. |
subscription.completed | The plan’s invoice limit was reached. |
subscription.cancelled | The subscription was cancelled. |
Payload
{
"event": "subscription.charge_succeeded",
"data": {
"subscription_code": "SUB_7q2m9x4k1v8b5n3p6r0s",
"status": "active",
"customer": {
"customer_code": "CUS_9x2m7q4v1k8b5n3p6r0s",
"email": "customer@example.com"
},
"plan": {
"plan_code": "PLN_4m8q1v2x7b9n3k6p5r0s",
"name": "Email Monthly",
"amount": "15000.00",
"currency": "NGN",
"interval": "monthly"
},
"payment_method": {
"payment_method_code": "PM_8r3n1v6q4m9x2k7b5p0s",
"brand": "mastercard",
"last4": "0008",
"exp_month": "01",
"exp_year": "39"
},
"next_charge_at": "2026-11-01T00:00:00+01:00",
"cancelled_at": null,
"metadata": {
"merchant_order_id": "EMAIL-ORDER-001"
},
"context": {
"invoice_code": "INV_3m7q9v2x5k8n1b4p6r0s",
"transaction_reference": "SUBCHG_7n2m9q4v1x8k5b3p6r0s",
"amount": "15000.00",
"currency": "NGN"
}
}
}Payload Fields
| Field | Type | Description |
|---|---|---|
event | string | Recurring lifecycle event name. |
data.subscription_code | string | Unique SaySwitch subscription code. |
data.status | string | Current subscription status. |
data.customer | object | Customer code and email. |
data.plan | object | Plan code, name, amount, currency, and interval. |
data.payment_method | object or null | Masked card display details. It is null before authorization. |
data.next_charge_at | string or null | Next scheduled charge time in ISO 8601 format. |
data.cancelled_at | string or null | Cancellation time in ISO 8601 format. |
data.metadata | object or null | Metadata supplied when the subscription was initialized. |
data.context | object or array | Event-specific information. Empty context is serialized as []. |
Event Context Examples
The top-level payload remains the same. Only event, subscription state, dates, payment method, and data.context vary.
Successful Scheduled Charge
{
"invoice_code": "INV_3m7q9v2x5k8n1b4p6r0s",
"transaction_reference": "SUBCHG_7n2m9q4v1x8k5b3p6r0s",
"amount": "15000.00",
"currency": "NGN"
}This is the context for subscription.charge_succeeded.
Failed Scheduled Charge
{
"invoice_code": "INV_3m7q9v2x5k8n1b4p6r0s",
"transaction_reference": "SUBCHG_7n2m9q4v1x8k5b3p6r0s",
"response_code": "51",
"message": "Insufficient funds"
}This is the context for subscription.charge_failed. Do not use a response code alone to decide whether to grant service; use the event name and retrieve the current subscription when needed.
Customer Authentication Required
{
"invoice_code": "INV_3m7q9v2x5k8n1b4p6r0s",
"transaction_reference": "SUBCHG_7n2m9q4v1x8k5b3p6r0s",
"authentication_type": "otp",
"authorization_url": "https://checkout.sayswitchgroup.com/pay/subscription/SUBINIT_example"
}For subscription.authentication_required, send the customer to the supplied authorization_url so they can continue on the SaySwitch-hosted checkout. Do not collect the OTP on your own server.
Subscription Activation
[]subscription.activated has an empty context. Use data.status, data.payment_method, and data.next_charge_at from the main payload.
Cancellation
[]subscription.cancelled has an empty context. Its payload has data.status set to cancelled, data.next_charge_at set to null, and data.cancelled_at set to the cancellation time.
An authorization failure carries a message in context. Initialized, authorization-expired, completed, and cancellation events otherwise use an empty context.
Handling Webhooks
- Read and retain the raw request body.
- Verify both signature headers using the keys for the event’s environment.
- Return a successful
2xxresponse promptly. - Process each event idempotently using the event name and subscription or transaction reference.
- Retrieve the subscription when you need to confirm its latest state.
Webhook deliveries can be retried. Your handler should safely accept the same event more than once.