Webhook integration
Receive, verify, and deduplicate WinkPG platform events at your own HTTPS endpoint.
WinkPG delivers asynchronous platform events (card transaction lifecycle, hosted payment page completion, invoice lifecycle, ACH settlement status, merchant creation, and more) to an HTTPS endpoint you control. This guide covers the wire format, the signature scheme, replay protection, and delivery semantics so your receiver validates and deduplicates events correctly.
The authoritative list of event types is the event catalog in the portal (Notifications then Event Types). Every type it lists is one the platform publishes today, and each entry carries a sample payload for that type. Build against the catalog rather than against a type name you have seen elsewhere.
Card transaction lifecycle events
Transaction.Authorized, Transaction.Declined, Transaction.Captured, Transaction.Voided, Transaction.Reversed, Transaction.Failed and Transaction.Settled cover a card payment's progress. Seven things to know before wiring order fulfilment to them:
Transaction.Capturedisn't a funds signal. Capture marks a transaction ready for settlement; funds move when the batch settles, which is whatTransaction.Settledreports.- One payment keeps one
TransactionId. A capture updates the original transaction in place rather than creating a new one. An authorization that's captured and later settles therefore raisesTransaction.Authorized,Transaction.CapturedandTransaction.Settled, all carrying the id you received at authorization. A refund is a separate transaction with its own id. Transaction.Settledis the funds signal, and it arrives hours later. Card settlement is a batch process, so this event lands well after the capture, and itsOccurredAtUtcis when the row settled rather than when the notification was sent.ClosedSettlementBatchId,GatewayBatchIdandProcessorBatchIdlet you reconcile a delivery against a settlement batch. ACH doesn't settle through this pipeline: useTransaction.AchStatusChangedfor those. If a settlement batch is rolled back and the transaction settles again later, a second event arrives with a different batch id.- Group settled events on
ClosedSettlementBatchId. It's the settlement batch's own identifier.GatewayBatchIdidentifies the settlement run, which closes one batch per processor, so it can't separate two batches settled in the same run.ClosedSettlementBatchIdisnullon a settlement applied outside the normal batch-close path and on transactions settled before the field was published; fall back to theGatewayBatchIdandProcessorBatchIdpair when it's absent. DeclinedandFailedare different outcomes.Declinedmeans the payment was refused (the issuer, the processor, or a fraud or policy rule said no), so retrying the same card unchanged won't help.Failedmeans the gateway couldn't process the request at all, so the payment was never decided and a retry may succeed.- A partial reversal is identifiable. On
Transaction.Reversed,IsPartialReversalistrueandAuthorizedAmountminusCumulativeReversedAmountis the amount still authorized. Each reversal of a transaction delivers its own event. - A partial approval has its own event. Subscribe to
Transaction.PartiallyApprovedto hear when the issuer approves less than you asked for. It carriesRequestedAmount,AuthorizedAmount,BalanceDueand theDispositionthe gateway resolved (Accepted,Voided, orPendingwhile an operator decides), and it isn't sent again when aPendingdisposition moves. When the gateway voids a partial approval on your behalf,Transaction.Voidedcarries aVoidReasonCode:PARTIAL_APPROVAL_ACKNOWLEDGMENT_TIMEOUTwhen nobody accepted the reduced amount before its deadline,PARTIAL_APPROVAL_AUTO_VOIDwhen your strategy voids partial approvals automatically,PARTIAL_APPROVALS_NOT_ACCEPTEDwhen you don't accept partial approvals, andPARTIAL_APPROVAL_VOIDEDotherwise. Every other void carriesnull.
Configure a destination
A webhook receiver is registered as a destination, scoped to a tenant, reseller, or merchant. Each destination carries:
- URL: the HTTPS endpoint that receives
POSTrequests. - Secret key: a per-destination shared secret used to sign every request. Required for production destinations; optional for sandbox. Stored encrypted at rest.
- Subscriptions: the set of event types the destination wants.
Rotate the destination secret on a defined cadence (90 days is a reasonable default). Rotation is non-disruptive: validate against both the previous and current secret during the rotation window.
Request format
Every delivery is an HTTPS POST with Content-Type: application/json. The body is a JSON envelope:
{
"id": "<unique event id, GUID>",
"type": "HostedPaymentPage.Transaction.Completed",
"version": 1,
"createdUtc": "2026-05-06T18:42:11.193Z",
"tenantId": "<guid>",
"merchantId": "<guid>",
"correlationId": "<guid>",
"data": { }
}
Use the X-WinkPG-Delivery-Id header as your idempotency key: it's identical on every retry and redelivery of the same delivery, and distinct for each endpoint the same event reaches. The envelope id (also sent as X-WinkPG-Event-Id) identifies the underlying event, so key on that one instead when a side effect must run at most once however many of your endpoints receive the event. The correlationId matches the id emitted in WinkPG logs and metrics for the originating operation, which makes cross-system tracing straightforward.
correlationId is your own value, echoed back
When your API request carries an X-Correlation-Id header, WinkPG adopts that value as the correlation id for the operation and returns it here on every event that operation raises. WinkPG generates a GUID only when you send no header.
That makes the field yours on both sides, so treat it as data you own:
- Send nothing personal or sensitive in it. Whatever you send is stored on the delivery record and delivered to every endpoint subscribed to the event, including endpoints owned by another party in your account hierarchy. An order number or an internal request id is a good value; a customer name, email address, or account number isn't.
- Keep it short and printable. WinkPG normalizes the value before storing or delivering it: surrounding whitespace is trimmed and control characters are removed. A value longer than 128 characters is omitted rather than shortened, so
correlationIdarrives asnulland you lose the trace. Shortening it would be worse: you would receive an id that looks real, matches nothing, and can't be told apart from one you sent. - Don't treat it as unique. Nothing stops two operations from carrying the same correlation id, because nothing but your own client chooses it. Use
X-WinkPG-Delivery-Idfor idempotency and the envelopeidfor event identity.
Signature scheme
Every request is signed with HMAC-SHA256 using the destination secret. The signature header is versioned:
X-WinkPG-Signature: v1=sha256:<hex-digest>
The signed input is the timestamp header, a literal ., then the exact raw request body:
"<X-WinkPG-Timestamp>.<raw request body>"
Compute HMAC_SHA256(secret_key, signature_input) and compare it to the received digest using a constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node) to avoid timing attacks. Check the v1=sha256: prefix before parsing the digest; the prefix exists so future algorithm upgrades don't break existing receivers.
The header can carry more than one signature
The header holds one entry per secret the endpoint is currently signed with, separated by commas:
X-WinkPG-Signature: v1=sha256:<current-digest>,v1=sha256:<incoming-digest>
That's one entry in normal operation and two while the endpoint's secret is being rotated. One X-WinkPG-Timestamp covers every entry, and the signed input is the same for each; only the key differs.
Split the header on commas, trim each entry, check its prefix, and accept the delivery when any entry matches your secret. Skip an entry whose prefix you don't recognize instead of rejecting the whole delivery. A receiver that assumes a single value, or that hex-decodes everything after the prefix without splitting first, rejects every delivery for the length of a rotation.
Rotating the signing secret
A secret is replaced through an overlap window, so your receiver is never locked out mid-cutover.
Update your receiver to read multiple signature values before the rotation is started. From the moment the overlap opens the header carries two entries. A receiver that reads only one rejects every delivery, and WinkPG stops delivering to an endpoint after three authentication failures inside fifteen minutes, so a mis-sequenced rotation takes your endpoint out of service until an administrator clears the suppression.
- Deploy verification code that splits the header, and confirm it's live on every instance. Deliveries still carry one entry at this point, so this is safe to do at any time.
- An administrator starts the rotation. The new secret is shown once and is never returned again. Deliveries begin carrying both signatures, the current secret's first.
- Configure the new secret on your receiver, instance by instance. Both signatures are present, so no coordinated cutover is needed.
- The administrator promotes the rotation. The new secret becomes the only signing secret, and deliveries return to a single entry.
If you can't be ready in time, ask for the rotation to be cancelled rather than promoted: the new secret is discarded and your current secret keeps working.
Replay protection
Two defenses work together:
- Timestamp validation: reject requests whose
X-WinkPG-Timestampis outside a tolerance window (plus or minus 5 minutes is recommended). The timestamp is part of the signed input, so an attacker can't alter it without invalidating the signature. - Deduplication: WinkPG delivers at least once, so the same request may be redelivered. Record processed
X-WinkPG-Delivery-Idvalues (a database row keyed on that id, or a RedisSETNXwith a generous TTL) and short-circuit duplicates with200 OK. Store the id in the same transaction as the side effect so a partial failure rolls back both. Headers have never been part of the signed input, so this header is additive and leaves signature verification unaffected.
Recovery of a missed card event
Card lifecycle events (the Transaction.* family) are raised on a best-effort path inside the payment pipeline. WinkPG never fails or delays a payment because a notification couldn't be raised, so an event can be missed at the moment of the transition.
A missed card event is re-emitted by a daily reconciliation and in almost all cases reaches your endpoint on the next run, as long as the transition happened within the last 48 hours. Treat it as a safety net rather than a delivery guarantee: recovery is bounded by that window, and a re-emission can itself fail and be retried on the following run. The re-emitted event carries the same X-WinkPG-Event-Id as the original and reports when the transition actually happened, so a receiver that deduplicates as described above needs no extra handling. Plan for two things: a recovered event can arrive after later events on the same transaction, and it carries no correlationId. Read the state in the payload rather than inferring it from the order events arrive in.
Response expectations
| Your receiver responds | WinkPG behavior |
|---|---|
2xx |
Success; no retry |
4xx other than 408 or 429 |
Permanent failure; no retry |
408, 429, or 5xx |
Transient; retried with exponential backoff |
| No response within timeout | Transient; retried with exponential backoff |
Aim to acknowledge within 5 seconds. If processing is expensive, validate the signature, enqueue the raw body, return 200 OK, then process asynchronously.
Security checklist
- Verify the signature with a constant-time compare.
- Split the signature header on commas and accept a match on any entry, so a secret rotation doesn't lock you out.
- Reject a delivery where no entry carries a recognized
v1=sha256:prefix. - Validate the timestamp against the current clock.
- Deduplicate on
X-WinkPG-Delivery-Id(use the event id instead only when a side effect must run at most once across every endpoint receiving the event). - Acknowledge with
2xxonly after the side effect is committed or durably enqueued. - Never log the secret key; rotate it on a cadence.