> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Payload Field FAQ

> Which field in a webhook body is your order number, where card BIN and last four actually live, what payout fee fields to consume, how a failed subscription period is expressed, and the ways a notification body differs from the matching inquiry response.

The event list and the "which inquiry does this mirror" mapping are on
[Webhook event type list](/docs/en/developer-docs/webhook/event-types); complete field
schemas are in [API Reference](/docs/api-reference/introduction). The delivery rules —
timeouts, accepted responses, retries, URL resolution — are in
[Webhook Delivery Contract FAQ](/docs/en/support/webhook-delivery-contract-faq).

**This page answers the field-level questions** that come up when you are writing
the handler and cannot find the value you expected.

## What is in the outer envelope?

Exactly two keys:

```json theme={null}
{
  "eventType": "PAYMENT_NOTIFICATION",
  "result": { }
}
```

There is **no** timestamp, no notification id, no version and no merchant id at
the top level, and the signature travels in the `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.

| Payload | Your identifiers | Waffo's identifiers |
| - | - | - |
| Payment | `merchantOrderId`, `paymentRequestId` | `acquiringOrderId` |
| Refund | `merchantRefundOrderId`, `refundRequestId`, `origPaymentRequestId` | `acquiringRefundOrderId`, `acquiringOrderId` |
| Subscription | `subscriptionRequest`, `merchantSubscriptionId` | `subscriptionId` |
| Payout | `payoutRequestId` | `payoutOrderId` |
| Chargeback | `merchantOrderId`, `originalPaymentRequestId` | `chargebackId`, `originalOrderId` |

Three notes that save time:

* **`paymentRequestId` is also the idempotency key** you generated for the
  request, which is why reusing it with a different amount or currency returns
  `A0011`. The same applies to `refundRequestId` and `subscriptionRequest`.
* **Chargeback names the disputed order `originalOrderId`**, not
  `acquiringOrderId` as 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](/docs/en/support/webhook-events-and-resend-faq).
* A payment or refund payload for a subscription charge also carries a nested
  `subscriptionInfo` with `subscriptionId`, `period` and `subscriptionRequest`,
  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 inside
`paymentInfo.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:

| Key | Content |
| - | - |
| `maskCardData` | First six and last four in one masked string, e.g. `402856******9917` |
| `cardBin` | The BIN, 6–8 digits |
| `cardLast4` | Last four digits |
| `cardExpiry` | Card expiry |
| `cardScheme` | Card scheme |
| `cardType`, `subCardType` | Card category |
| `cardIssueCountryCode` | Issuing country |
| `cardIssuerName` | Issuer name |
| `schemeTransactionId` | Scheme-level transaction reference |
| `tokenId` | The token, when the payment used one |

Other keys in the same string cover exchange rate, the customer's paid amount and
currency, and a pay-method reference — see
[API Reference](/docs/api-reference/introduction) 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](/docs/en/support/fee-and-tax-treatment-faq).

## 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:

| Field | Meaning |
| - | - |
| `actFeeAmount` | The fee actually collected when the transaction completed |
| `feeCurrency` | The currency `actFeeAmount` is denominated in |
| `feeStatus` | `DEDUCTED`, `RETURNED` or `NONE` |

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:

| Field | What it really is |
| - | - |
| `paymentInfo.payMethodUserAccountNo` (with `payMethodUserAccountType` of `EMAIL`, `PHONE_NO` or `ACCOUNT_ID`) | The account identifier **you supplied** at order creation, echoed back |
| The bank details inside `orderAction` | The Waffo-side account the customer pays **into** — the opposite direction |
| `userInfo.userBankInfo` on a **refund** payload | Bank details **you supplied on the refund request**, required by some payment method types to execute the payout leg |

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](/docs/en/support/refund-funding-source-faq) and
[Payment method refund limits](/docs/en/support/payment-method-refund-limits-faq).

## What does an unpaid, cancelled or timed-out order look like in the payload?

All three arrive as `PAYMENT_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:

| `orderStatus` | Terminal? | Meaning |
| - | - | - |
| `AUTHORIZATION_REQUIRED` | No | Waiting for the customer to authorize |
| `PAY_IN_PROGRESS` | No | Payment under way |
| `AUTHED_WAITING_CAPTURE` | No | Authorized; awaiting your capture |
| `CAPTURE_IN_PROGRESS` | No | Capture under way |
| `PAY_SUCCESS` | Yes | Paid |
| `ORDER_CLOSE` | Yes | Not paid |

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:

| Field | Content |
| - | - |
| `acquiringOrderId` | The period's payment order |
| `period` | The period number |
| `orderCurrency`, `orderAmount` | The charge for that period |
| `orderStatus` | The period order's status |
| `orderUpdatedAt` | When it last changed |
| `topupDetails` | Add-on orders for that period, when there are any |

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[].orderStatus` is the period order's own status, not the
  merchant-display order status.** Besides `PAY_SUCCESS` and `ORDER_CLOSE` it can
  be `INIT`, `TO_PAY` or `NEED_RETRY` while a charge is still pending or
  retrying. Treat only the first two as final.

A related asymmetry: the same name means two different things in the subscription
inquiry contract. On the **request** `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 receive **`SUBSCRIPTION_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:

| Where | What it says |
| - | - |
| `subscriptionStatus` | Still `ACTIVE` — a failed renewal period does not close a subscription |
| `paymentDetails[].period` | Which period failed |
| `paymentDetails[].orderStatus` | `ORDER_CLOSE` for that period |
| `productInfo.currentPeriod` | The period the subscription is on |

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](/docs/en/developer-docs/webhook/event-types)
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:

| Which period exhausted its retries | Subscription outcome | Event |
| - | - | - |
| The first charge (including a deferred-start activation charge) | Subscription goes to `CLOSE` | `SUBSCRIPTION_STATUS_NOTIFICATION` with `subscriptionStatus` `CLOSE` |
| A renewal period | Subscription **stays `ACTIVE`**, and the next period is still attempted | `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` only — no status notification |

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](/docs/en/support/subscription-retry-and-payment-methods-faq).

## 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:

| `subscriptionStatus` | Set by |
| - | - |
| `IN_PROGRESS` | Subscription created, first charge under way |
| `AUTHORIZATION_REQUIRED` | Waiting for the customer to authorize |
| `ACTIVE` | Active |
| `CLOSE` | Closed after the first charge finally failed |
| `MERCHANT_CANCELLED` | Your cancel API call, or a cancel from the Portal |
| `USER_CANCELLED` | The customer cancelling on the wallet or subscription-management page |
| `CHANNEL_CANCELLED` | The underlying payment instrument being unbound on the payment method's side |
| `PLATFORM_CANCELLED` | Cancelled by Waffo — the only status that populates `cancelReason` |
| `EXPIRED` | The subscription's end time passed |

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.

| Payload | Identical to inquiry `data`? |
| - | - |
| Payment, refund, chargeback, subscription change | Yes — built by the same code as the corresponding inquiry |
| Subscription status / period changed | **No.** The notification **always** includes the full `paymentDetails` array; a subscription inquiry omits it unless you ask for it |
| Payout | Same type as the payout inquiry, plus the `notifyUrl` you passed is echoed back in the body |

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-SIGNATURE` over the raw bytes as received. See
  [Webhook signature verification](/docs/en/developer-docs/webhook/signature-verification).

## Can I have a complete, real example body for every event type?

Per-event examples are maintained in
[API Reference](/docs/api-reference/introduction) rather than here, because each
`result` 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
  `payMethodResponse` and 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](/docs/en/support/sandbox-simulation-limits-faq).

***


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.