A webhook is a signed notification that a consent, payment or refund changed status, or that a Fixed Recurring Payment instalment completed, failed or was cancelled. It carries IDs only: call the GET endpoint for the details. You subscribe at merchant-account level, and BlinkPay POSTs each subscribed event to your callback URL.
Webhooks are best effort
An event can occasionally arrive more than once, or not at all if either side fails. Deduplicate on event_id and reconcile through the GET endpoints.
The absence of an event is not a status: if nothing has arrived, read the resource rather than assuming it hasn't changed. Webhooks remove routine polling, not the periodic reconcile, so keep a sweep that reads anything still open. There is no ordering promise: a later event can arrive before an earlier one, so never infer status from arrival order. Nothing is sent after a consent or payment reaches a final status.
consent-updated event fires then.Every event type starts with urn:nz:co:blinkpay:debit:events:.
| Event type | Fires when | IDs carried |
|---|---|---|
urn:nz:co:blinkpay:debit:events:consent-updated | A consent becomes Authorised, Rejected, Revoked or GatewayTimeout, for every consent type and flow, including decoupled and card; or an enduring consent becomes Consumed | consent_id |
urn:nz:co:blinkpay:debit:events:payment-updated | A payment becomes AcceptedSettlementCompleted or Rejected, including Fixed Recurring Payment instalments | consent_id, payment_id |
urn:nz:co:blinkpay:debit:events:refund-updated | A card refund becomes completed or failed | consent_id, payment_id, refund_id |
urn:nz:co:blinkpay:debit:events:fixed-recurring-payment-completed | An instalment has been accepted by the bank | consent_id, frp_id, payment_id |
urn:nz:co:blinkpay:debit:events:fixed-recurring-payment-failed | An instalment has failed terminally and the schedule has advanced to the next period | consent_id, frp_id, payment_id if a payment record was created |
urn:nz:co:blinkpay:debit:events:fixed-recurring-payment-cancelled | The schedule has been cancelled, through the API or because the consent was revoked or expired | consent_id, frp_id, payment_id only if the cancellation interrupted an instalment whose payment had already been submitted |
A single consent or quick payment that becomes Consumed sends no consent-updated; the payment-updated for its payment covers it. An account_number refund sends no event: no money moves and it is completed as soon as you create it. A card refund still processing can take up to about 12 hours to send its event, unless you read the refund first.
An instalment that created a payment raises both its Fixed Recurring Payment event and payment-updated, so subscribing to both delivers two events for it. Use frp_id and consent_id as your correlation keys for Fixed Recurring Payment events.
A fixed-recurring-payment-failed event means the bank declined the instalment or did not respond; the API does not say which. The payment carries no rejection reason, so GET /payments/v1/payments/{payment_id} shows only that it is Rejected. failed means "this debit didn't go through, but the schedule is still active and will try again next period": typically prompt the customer to update their payment details, but don't end their subscription. cancelled means "the schedule itself is gone and no more debits will happen": typically revert the customer to a non-active state and ask them to re-authorise if you want them back.
| Field | Type | Notes |
|---|---|---|
event_type | string (URN) | One of the six event types above. |
event_id | UUID | Unique to the status change, and the same across retries and across your subscriptions. Deduplicate on it. |
timestamp | ISO 8601 with offset | When BlinkPay recorded the status change, in NZ time. |
consent_id | UUID | The consent the event belongs to. Always present. |
payment_id | UUID, conditional | Present on payment and refund events, and on Fixed Recurring Payment events as listed above. |
refund_id | UUID, conditional | Present on refund events. |
frp_id | UUID, conditional | Present on the three Fixed Recurring Payment events only; an instalment's payment-updated does not carry it. |
A field that doesn't apply to an event is absent, not null, so check for the key's presence. Payloads carry no names, account numbers, amounts or PCR fields; read those with your normal API credentials.
Consent:
{
"event_type": "urn:nz:co:blinkpay:debit:events:consent-updated",
"event_id": "23456789-2345-2345-2345-234567890123",
"timestamp": "2026-10-01T09:45:00+13:00",
"consent_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}
Payment:
{
"event_type": "urn:nz:co:blinkpay:debit:events:payment-updated",
"event_id": "12345678-1234-1234-1234-123456789012",
"timestamp": "2026-10-01T09:15:04+13:00",
"consent_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"payment_id": "87654321-4321-4321-4321-210987654321"
}
Refund:
{
"event_type": "urn:nz:co:blinkpay:debit:events:refund-updated",
"event_id": "34567890-3456-3456-3456-345678901234",
"timestamp": "2026-10-02T18:04:12+13:00",
"consent_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"payment_id": "87654321-4321-4321-4321-210987654321",
"refund_id": "45678901-4567-4567-4567-456789012345"
}
Fixed Recurring Payment:
{
"event_type": "urn:nz:co:blinkpay:debit:events:fixed-recurring-payment-completed",
"event_id": "56789012-5678-5678-5678-567890123456",
"timestamp": "2026-11-01T09:00:41+13:00",
"consent_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"frp_id": "11111111-2222-2222-2222-333333333333",
"payment_id": "98765432-9876-9876-9876-987654321098"
}
Subscriptions are managed through the API and apply to your whole merchant account. Each one lists the event types it receives, and you can register several, for example to send different events to different URLs. The access token needs the create:subscription, view:subscription or delete:subscription scope.
| Method | Path | Description |
|---|---|---|
POST | /payments/v1/subscriptions | Create a subscription |
GET | /payments/v1/subscriptions | List subscriptions |
DELETE | /payments/v1/subscriptions/{subscription_id} | Delete a subscription |
Request:
POST /payments/v1/subscriptions HTTP/1.1
Host: sandbox.debit.blinkpay.co.nz
Authorization: Bearer {access_token}
Content-Type: application/json
{
"callback_url": "https://yourapp.example.com/webhooks/blinkpay",
"event_types": [
"urn:nz:co:blinkpay:debit:events:consent-updated",
"urn:nz:co:blinkpay:debit:events:payment-updated",
"urn:nz:co:blinkpay:debit:events:refund-updated"
]
}
Response:
{
"subscription_id": "9f8e7d6c-5b4a-3c2b-1a09-8f7e6d5c4b3a",
"callback_url": "https://yourapp.example.com/webhooks/blinkpay",
"event_types": [
"urn:nz:co:blinkpay:debit:events:consent-updated",
"urn:nz:co:blinkpay:debit:events:payment-updated",
"urn:nz:co:blinkpay:debit:events:refund-updated"
],
"secret": "whsec_a1b2c3d4e5f6",
"creation_timestamp": "2026-01-14T09:00:00+13:00"
}
| Field | Description |
|---|---|
subscription_id | Unique identifier for the subscription. |
callback_url | The URL BlinkPay POSTs events to. Redirects are not followed. |
event_types | The event types this subscription receives. |
secret | Signing secret (whsec_… prefix) for this subscription. Store it immediately: it is returned only on create. |
creation_timestamp | When the subscription was created (ISO 8601). |
The callback URL must be HTTPS in production and on the same whitelist as your redirect URIs (Client Portal, Settings > API Settings, Whitelisted Redirect URLs, or ask support). A URL that isn't on it is refused with 400 BP214. Sandbox and production subscriptions are separate, with separate secrets and separate whitelists, so create the subscription again when you go live.
There is no pause: to stop deliveries, delete the subscription. To rotate a secret, create a new subscription, verify its events, then delete the old one; while both exist, each receives every matching event signed with its own secret.
Each delivery carries two headers besides Content-Type: application/json:
X-Signature: t=1739545800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-Idempotency-Key: 12345678-1234-1234-1234-123456789012
t is a Unix timestamp, re-stamped on every attempt, so a retry carries a new t and a new v1. To verify:
t and v1 from the header.1739545800.{...body...}.HMAC-SHA256(signed_string, your_signing_secret) and hex-encode it in lowercase. Treat the signed string and the secret as UTF-8 bytes, and use the full secret, including the whsec_ prefix, as the key.v1 with a constant-time comparison.t is more than 5 minutes old (allowing ±60 seconds of clock skew).| Input | Value |
|---|---|
| Secret | whsec_test_secret |
Timestamp (t) | 1739545800 |
| Raw body | {"event_id":"abc"} |
| Signed string | 1739545800.{"event_id":"abc"} |
Expected v1 | 6fc0de6006119436f09db0d062c6d181b7e10778533152586b5a03c0b93b73bc |
If you get a different v1 for the same inputs, re-check the body bytes, the secret encoding (UTF-8, prefix included) and the hex case.
Use the raw body, not a re-serialised object
Most web frameworks parse JSON before your handler runs, and re-serialising the parsed object will not reproduce the bytes that were signed. Capture the raw body before parsing.
Any 2xx response is a successful delivery. Connection errors, timeouts, 5xx and 429 are retried; other 4xx and 3xx fail fast. Your endpoint has 10 seconds to respond to each attempt, and there are up to 4 attempts (initial + 3). An event that fails every attempt is not sent again; your periodic reconcile picks up the change.
Reading the resource and updating your records normally fits inside the 10 seconds. If your handling can take longer, respond 2xx first and finish afterwards: an attempt that times out is retried, so the event arrives again. Because redirects are not followed, a callback URL that redirects, for example to add www or a trailing slash, fails every delivery.
An event only tells you to read the resource, so a repeat is harmless if you update your records only where they don't already match its status. Where an event triggers something that must happen once, such as an email to the customer, key it on event_id; X-Idempotency-Key carries the same value.
Each event comes from real sandbox activity, so walk a consent or payment through the flow with the sandbox test values. Create a sandbox subscription first; its callback URL must be on your sandbox whitelist, where http:// and localhost are allowed.
| Event | How to produce it in sandbox |
|---|---|
consent-updated | Authorise a consent: the decoupled auto-approve number is quickest. Cancel on the PNZ login page for Rejected; call the consent's DELETE endpoint after authorising it for Revoked; leave a gateway consent without choosing a bank for GatewayTimeout. |
payment-updated | Make a payment on an authorised consent, or complete a quick payment, and wait for AcceptedSettlementCompleted. Cancelling a quick payment on the PNZ login page gives a Rejected payment. |
refund-updated | Refund a sandbox card payment with full_refund or partial_refund, if card refunds are enabled on your sandbox account. |
fixed-recurring-payment-completed | Create a Fixed Recurring Payment with a small amount and a start_date of today, before 09:00 NZ time, then wait. |
fixed-recurring-payment-cancelled | Call DELETE /payments/v1/fixed-recurring-payments/{fixed_recurring_payment_id}. |
fixed-recurring-payment-failed | Not triggerable on demand in the sandbox. |
A Gateway consent whose customer leaves without choosing a bank now becomes GatewayTimeout once the 15-minute Gateway session cutoff passes, even if you don't poll, and consent-updated fires for it. Its status_updated_timestamp can therefore be earlier than your polls used to see. A customer who cancels at the Gateway after that leaves it GatewayTimeout rather than Rejected. See Timeouts and Deadlines.