What is in the outer envelope?
Exactly two keys:X-SIGNATURE header rather than
in the body. So dispatch on eventType, and take everything else from result.
Should I dispatch on eventType or on something inside result?
Dispatch on eventType. It tells you which event family you have and therefore
the shape of result; the status fields inside result then tell you what
happened. Doing it the other way round breaks on the cases where one event type
carries several statuses — a payment notification, for example, is the same
eventType at the authorization stage and at the final state.
The rule of thumb: eventType selects the parser, result’s status selects the
business branch.
Which field is my own order number?
Every payload carries both sides of the identity — yours and Waffo’s. Use yours as the join key into your database, and Waffo’s for support conversations.
Three notes that save time:
paymentRequestIdis also the idempotency key you generated for the request, which is why reusing it with a different amount or currency returnsA0011. The same applies torefundRequestIdandsubscriptionRequest.- Chargeback names the disputed order
originalOrderId, notacquiringOrderIdas every other payload does. It is the one payload where the naming breaks pattern, and searching a chargeback by your own order number in the Portal webhook log will not find it — see Webhook Event Dictionary & Batch Replay. - A payment or refund payload for a subscription charge also carries a nested
subscriptionInfowithsubscriptionId,periodandsubscriptionRequest, which is how you attribute a payment order to a billing period.
Where are the card BIN and the last four digits?
Not at the top level. Card details live insidepaymentInfo.payMethodResponse, and the important structural detail is that
payMethodResponse is a JSON-encoded string, not a nested object — you
must parse it before you can read anything out of it.
Once parsed, the card-related keys are:
Other keys in the same string cover exchange rate, the customer’s paid amount and
currency, and a pay-method reference — see
API Reference for the published description.
When are they populated? Only for card-type payments — card, and the two
mobile wallet products that present a card underneath. For any other payment
method, no card keys appear at all. So write your parser to treat every one of
them as optional; a missing
cardBin is normal, not an error.
firstSix, first6, lastFour, last4, maskedCardNumber and cardNo are
not field names in any Waffo payload. If you are looking for one of those,
you want maskCardData, cardBin or cardLast4.
Can I get userFee from an inquiry or a notification?
No. userFee is not a response field in any merchant-facing inquiry or
notification payload.
The name does appear in the description of the orderAction data used when you
bypass the hosted cashier — but those are keys of that action payload, not fields
of an order. For the amounts and fees that are returned on an order, see
Fee and tax treatment.
Payout has actFeeAmount — is feeAmount still there? Which do I consume?
Consume actFeeAmount. On the merchant-facing payout payload, feeAmount is
not present — actFeeAmount replaced it, and it is the field the published
contract carries.
Read it together with its two companions:
The relationship matters for your bookkeeping:
actFeeAmount is the collected
amount when feeStatus is DEDUCTED, and 0 when feeStatus is RETURNED
or NONE. So do not read a zero fee as “free” without checking feeStatus —
a returned fee and a never-charged fee both show zero and mean different things.
One naming trap: feeAmount does still exist on the chargeback payload,
where it means the chargeback fee. Same field name, unrelated quantity — do not
share a deserializer between the two.
Payout outcome fields, for completeness: status is IN_PROGRESS, SUCCESS or
FAILURE; a reversal is expressed by subStatus of FULL_REVERSED or
PARTIAL_REVERSED; a failure carries failureCode and failureReason.
After a customer pays, can I get their bank account so I refund to the same account?
No. No payin notification or order inquiry returns the customer’s bank account number or account holder name. Three fields look like they might be it, and none are:
The reassuring part of the answer: for the payment methods where refunding to the
original instrument matters, the refund is executed back along the original
payment rather than to an account you nominate — so you do not need the account
number to get that behaviour. Where a refund does require bank details, the API
asks you for them. See
Refund funding source and
Payment method refund limits.
What does an unpaid, cancelled or timed-out order look like in the payload?
All three arrive asPAYMENT_NOTIFICATION and all three land on the same
terminal status — ORDER_CLOSE. There is no separate cancelled, expired or
failed status value.
The complete set of order statuses you can receive:
To distinguish cancel from timeout from decline, read
orderFailedReason —
also a JSON-encoded string, shaped
{"orderFailedCode":"...","orderFailedDescription":"..."}. It is present only
when the status is ORDER_CLOSE; a successful or in-flight order omits it
entirely, so your parser must tolerate its absence rather than defaulting it.
The refund payload’s equivalent is refundFailedReason, and a subscription’s is
failedReason, both in the same JSON-string shape. The names failReason,
errorCode, errorMsg, channelErrorCode and merchantErrorCode do not
exist in these payloads.
Is paymentDetails on a subscription the full history? Does it keep growing?
Yes to both, and this is worth designing for before you have long-lived
subscribers. paymentDetails is an array of every period up to the current
one, ordered by period ascending, with no cap, no page size and no cursor.
A monthly subscription running for years grows the array by one element per
period, and the payload grows with it.
Each element carries:
Two practical consequences:
- Stream or index it, do not hold it whole. Read the periods you care about —
usually the highest
period, or the ones you have not recorded yet — rather than diffing the entire array on every notification. paymentDetails[].orderStatusis the period order’s own status, not the merchant-display order status. BesidesPAY_SUCCESSandORDER_CLOSEit can beINIT,TO_PAYorNEED_RETRYwhile a charge is still pending or retrying. Treat only the first two as final.
paymentDetails is a flag (0 or 1,
default 0) asking for the array; on the response it is the array.
My subscription is still ACTIVE but one period’s charge failed. What do I receive?
You receiveSUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION, and only after that
period’s retries are exhausted. Intermediate failed attempts fire no
notification at all — the period order goes to NEED_RETRY and waits.
What the payload tells you:
Set expectations about the reason, because this is a real limitation: the
top-level
failedReason is the subscription’s failure detail, not the failed
period’s, so it does not reliably describe why that period was declined, and the
period elements carry no failure code of their own. To get a per-attempt failure
reason, subscribe to PAYMENT_NOTIFICATION as well — it fires for every payment
attempt and carries that attempt’s orderFailedReason. This is precisely the
distinction the
subscription notification selection guide
makes between the “minimal” and “full” combinations.
If renewal retries run out, is the subscription cancelled?
It depends which period failed, and the two cases behave differently:
So do not treat an exhausted renewal as a cancellation, and do not revoke
entitlements on a period-changed notification alone. If your product should
suspend access after a failed period, that is your policy to implement on the
period event — Waffo does not close the subscription for you.
How many retries? There is no single number: the retry count and intervals
come from your merchant agreement’s renewal retry strategy, resolved per period
type — daily, weekly, monthly, quarterly, yearly, or a default. Confirm the
figures configured for your account with integration support rather than assuming
a number, and see
Subscription retry and payment methods.
The customer cancelled inside their wallet app. Which status do I get?
SUBSCRIPTION_STATUS_NOTIFICATION with subscriptionStatus of
USER_CANCELLED.
The full status set, so you can branch on all of it:
Note that
PLATFORM_CANCELLED is missing from the published enum list on the
subscription status field even though it can be delivered, so handle it as part
of the cancellation family — MERCHANT_CANCELLED, USER_CANCELLED,
CHANNEL_CANCELLED and PLATFORM_CANCELLED all mean “no longer billing”.
Is a notification body really identical to the inquiry response?
Mostly, but not always — and the exception is the one that will surprise you.
Two implications:
- Do not build your subscription handler by diffing against an inquiry response. The extra array is expected, not a bug, and it is the reason a subscription webhook can be dramatically larger than the inquiry you compared it with.
- Do not assume key order is stable. Field ordering in a notification body is
not guaranteed to match anything, which matters only if you were planning to
hash or canonicalise the body yourself. You do not need to: verify the
X-SIGNATUREover the raw bytes as received. See Webhook signature verification.
Can I have a complete, real example body for every event type?
Per-event examples are maintained in API Reference rather than here, because eachresult is the corresponding inquiry response and the API Reference is generated
from the live contract — which makes it the only copy that cannot drift.
Two cautions when you use it as your fixture source:
- The published reference is regenerated periodically, so a field that exists in
the payload can be missing from the reference for a while. Card keys inside
payMethodResponseand subscription add-on details are current examples: they are delivered, and only partly described. Treat the payload as a superset and make your parser tolerant of unknown fields. - Do not build fixtures from a document when you can capture the real thing. Run the flow in the sandbox and record what arrives at your endpoint — that is a payload for your merchant configuration, with your payment methods, which is what your handler actually has to survive. See Sandbox Simulation Limits FAQ.