Dedicated URL — Unlike
transaction_notif_url which is shared across VA, QRIS, Payment Link, and E-Wallet events, the subscription_cycle_notif_url is exclusively for subscription events. Configure it separately in the merchant dashboard.Information
| Method | Path | Format | Authentication |
|---|---|---|---|
| POST | https://your-webhook-url/callback | json | HMAC SHA512 Signature |
- A cycle charge succeeds — initial charge after card linking, every recurring cycle, or a successful retry
- A cycle charge fails — initial attempt and every retry attempt are delivered separately
- The plan status changes — e.g.
suspendedafter retries exhausted,completedafter the last cycle, orcancelled(including system-initiated auto-cancel forcharge_immediately = trueplans rejected by the issuer)
event field distinguishes between them.
Events
| Event | When it fires | success field |
|---|---|---|
subscription.cycle.payment_success | A cycle bill is paid (initial, recurring, or successful retry) | true |
subscription.cycle.payment_failed | A cycle bill attempt fails — fired on every attempt including each retry | true (HTTP 200) |
subscription.plan.status_changed | The plan transitions to suspended, cancelled, or completed | true |
success: true on payment_failed events indicates the webhook itself was emitted successfully — not that the charge succeeded. Always check the event field to determine the actual outcome.Request Details
Headers
| Field | Value | Type | Mandatory | Description |
|---|---|---|---|---|
Content-Type | application/json | Alphabetic | Yes | Specifies JSON format for the request body |
User-Agent | SingaPaymentGateway/1.0 | Alphabetic | Yes | Identifies the source of the webhook |
Accept | application/json | Alphabetic | Yes | Expected response format |
X-PARTNER-ID | — | Alphanumeric | Yes | Your API Key from the merchant dashboard |
X-Signature | — | Alphanumeric | Yes | HMAC SHA512 signature (128 chars) for request verification |
X-Timestamp | — | Numeric | Yes | Unix timestamp in seconds when the request was sent |
Authorization | Bearer <random_token> | Alphanumeric | Yes | System-generated random bearer token; used as a component in the signature |
The
Authorization token is a randomly generated string — not a user access token. Extract it as-is and use it in the string to sign. The signing scheme is identical to other SingaPay webhooks. See How to Validate Signature below.Body Parameters — Payment Success / Failed
These two events share the same body shape. The only difference is theevent value and, on failure, whether bill.retry indicates more attempts are coming.
HTTP-style status code. Always
200.Always
true — indicates the webhook was emitted successfully, not the charge outcome."subscription.cycle.payment_success" or "subscription.cycle.payment_failed".Event timestamp in format
"d M Y H:i:s". Example: "01 May 2026 00:00:15"Event payload.
Payload Examples — Payment Events
Body Parameters — Plan Status Changed
Fires when the plan transitions tosuspended, cancelled, or completed.
Always
200.Always
true."subscription.plan.status_changed"Event timestamp in format
"d M Y H:i:s". Example: "07 May 2026 00:05:00"Payload Example — Plan Status Changed
Plan suspended after retries exhausted
Security and responses
Return HTTP200 promptly after validating the request. For retry behavior, see Webhook retry mechanism.
Verify every webhook using Security and signature validation. Use your configured callback path when building StringToSign.
Handle duplicate deliveries idempotently using stable identifiers from the payload (for example transaction_id or reff_no).
Idempotency
Usebill.bill_number as your primary idempotency key when storing successful deliveries. The same bill number may arrive more than once if your endpoint returned a non-2xx response and SingaPay retried the webhook delivery.
For failed charge retries, the bill_number stays the same but bill.retry.attempt increments. Use the compound key (bill_number, retry.attempt) if you need per-attempt idempotency.
Important Notes
One event per attempt
One event per attempt
Every retry attempt emits its own
subscription.cycle.payment_failed (or payment_success on the attempt that finally clears). Monitor bill.retry.attempts_remaining to know whether another automatic attempt is coming.Plan status events are separate
Plan status events are separate
When retries are exhausted with
failed_payment_action: stop_plan, expect a payment_failed event followed shortly by a plan.status_changed event with plan.status: "suspended". These are two distinct webhook deliveries.Plan lineage after upgrades
Plan lineage after upgrades
After a PATCH upgrade, the new plan has a different
plan.id but the same plan.subscription_id and plan.merchant_reff_no. The old plan’s ID is available on plan.parent_plan_id for reconciliation.merchant_reff_no is optional
merchant_reff_no is optional
If you didn’t set
merchant_reff_no at plan creation, it will be null on every cycle webhook. Set it during plan creation to have it echoed back reliably for your own tracking.Currency is always IDR
Currency is always IDR
bill.currency is always "IDR" today — card recurring runs in IDR only.