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.

HeaderDescription
payloadSignatureHMAC SHA512 signature of the raw JSON request body using your secret key.
merchantSignatureHMAC SHA512 signature of your public key using your secret key.
timestampISO 8601 timestamp generated when the webhook is sent.
Content-Typeapplication/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

EventMeaning
subscription.initializedA subscription was created and is waiting for customer authorization.
subscription.activatedCard authorization and consent succeeded, and the subscription became active.
subscription.authorization_failedThe hosted card authorization attempt failed.
subscription.authorization_expiredThe customer did not complete authorization before the session expired.
subscription.authentication_requiredThe customer must complete an authentication step for the initial charge.
subscription.charge_succeededA billing invoice was charged successfully.
subscription.charge_failedA billing invoice charge failed. The subscription may become past_due.
subscription.completedThe plan’s invoice limit was reached.
subscription.cancelledThe 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

FieldTypeDescription
eventstringRecurring lifecycle event name.
data.subscription_codestringUnique SaySwitch subscription code.
data.statusstringCurrent subscription status.
data.customerobjectCustomer code and email.
data.planobjectPlan code, name, amount, currency, and interval.
data.payment_methodobject or nullMasked card display details. It is null before authorization.
data.next_charge_atstring or nullNext scheduled charge time in ISO 8601 format.
data.cancelled_atstring or nullCancellation time in ISO 8601 format.
data.metadataobject or nullMetadata supplied when the subscription was initialized.
data.contextobject or arrayEvent-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

  1. Read and retain the raw request body.
  2. Verify both signature headers using the keys for the event’s environment.
  3. Return a successful 2xx response promptly.
  4. Process each event idempotently using the event name and subscription or transaction reference.
  5. 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.