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

# Order Creation Field Guide

> The complete /api/v1/order/create request field table with every length limit, which fields are mandatory, what happens when a value is too long, how the paymentRequestId idempotent check actually behaves when you resend, and the field rules behind the common validation errors.

This page explains the order-creation fields merchants ask about most, the
complete field table with its length limits, and the validation rules behind
the most common errors.

## The three IDs, and which one to use where

| Field | Who generates it | Purpose |
| - | - | - |
| `paymentRequestId` | **You** (String 32) | Identifies this create **request**, and is used for the idempotent check. Reuse the same value to safely retry the same order; a different order must use a new value. |
| `merchantOrderId` | **You** (String 64) | Your business order number, carried through for your own reference and reconciliation. |
| `acquiringOrderId` | **Waffo** (String 32) | Waffo's order ID, returned in the create response. Use it in follow-up calls such as inquiry and refund, and to match webhook notifications. |

**Idempotency in practice:** retrying with the same `paymentRequestId` and
identical parameters returns the same order instead of creating a duplicate.
Retrying with the same `paymentRequestId` but **different** parameters is
rejected with `A0011 Idempotent param mismatch error`.

## Is there one table showing which fields are mandatory and how long they can be?

Yes — this one. "Mandatory" here means *mandatory for every order*. A second,
larger group of fields is optional in the contract but becomes mandatory for
particular payment methods or scenarios; those are listed separately in
[Conditionally Required Fields by Payment Method](/docs/en/support/conditional-required-fields-faq),
and the lookup table is in [API Reference Tables](/docs/en/support/api-reference-tables).

**Top level**

| Field | Mandatory | Max length |
| - | - | - |
| `paymentRequestId` | Yes | 32 |
| `merchantOrderId` | Yes | 64 |
| `orderCurrency` | Yes | 12 |
| `orderAmount` | Yes | 24 |
| `orderDescription` | Yes | 128 |
| `orderRequestedAt` | Yes | — (timestamp) |
| `notifyUrl` | Yes | 1024 |
| `orderExpiredAt` | No | — (timestamp) |
| `userCurrency` | No | 12 |
| `successRedirectUrl` | No | 1024 |
| `failedRedirectUrl` | No | 1024 |
| `cancelRedirectUrl` | No | 512 |
| `extendInfo` | No | 128 |
| `paymentTokenData` | No | 8192 |
| `promotionInfo.promotionCode` / `.couponId` | No | 64 each |

**`merchantInfo`** (the object itself is mandatory)

| Field | Mandatory | Max length |
| - | - | - |
| `merchantId` | Yes | 64 |
| `subMerchantId` | No | 64 |

**`userInfo`** (the object itself is mandatory)

| Field | Mandatory | Max length |
| - | - | - |
| `userId` | Yes | 64 |
| `userEmail` | Yes | 64 |
| `userTerminal` | Yes | 32 |
| `userPhone` | No | 30 |
| `userCountryCode` | No | 3 |
| `userFirstName` / `userLastName` | No | 64 each |
| `userCreatedAt` | No | — (timestamp) |
| `userBrowserIp` | No | 128 |
| `userAgent` | No | 256 |

**`paymentInfo`** (the object itself is mandatory)

| Field | Mandatory | Max length |
| - | - | - |
| `productName` | Yes | 32 |
| `payMethodType` | No | 256 |
| `payMethodName` | No | 256 |
| `payMethodProperties` | No | 1024 |
| `payMethodCountry` | No | 3 |
| `payOption` | No | — |
| `userPaymentAccessToken` | No | 256 |
| `payMethodUserAccountNo` | No | 64 |
| `payMethodUserAccountType` | No | 24 |
| `cashierLanguage` | No | 24 |
| `cashierAppearance` | No | — (JSON string) |
| `receiptTemplateId` | No | 32 |
| `captureMode` | No | 24 |
| `merchantInitiatedMode` | No | 24 |

**`goodsInfo`** — the object is optional. The contract documents `goodsName`
and one of `goodsUrl` / `appName` as expected content (for compliance and risk
review), but the create request only enforces the length limits below; whether a
payment method insists on them is a per-method conditional rule

| Field | Max length |
| - | - |
| `goodsName` | 64 |
| `goodsUrl` | 128 |
| `appName` | 32 |
| `goodsId` | 32 |
| `skuName` | 32 |
| `goodsCategory` | 32 |
| `goodsUniquePrice` | 16 |
| `goodsQuantity` | — (integer) |

**`cardInfo`**, **`riskData`**, **`addressInfo`**, **`brandInfo`** — all optional objects

| Field | Max length |
| - | - |
| `cardInfo.cardNumber` | 24 |
| `cardInfo.cardCvv` | 4 |
| `cardInfo.cardHolderName` | 128 |
| `cardInfo.threeDsDecision` | 12 |
| `cardInfo.cardExpiryYear` / `cardExpiryMonth` | — (integer) |
| `riskData.userType` / `userCategory` | 24 each |
| `riskData.userLegalName` / `userDisplayName` | 128 each |
| `riskData.userRegistrationIp` / `userLastSeenIp` | 24 each |
| `riskData.userIsNew` / `userIsFirstPurchase` | 8 each |
| `addressInfo.address` | 256 |
| `addressInfo.city` / `region` | 32 each |
| `addressInfo.postcode` | 16 |
| `addressInfo.addressCountryCode` | 16 |
| `brandInfo.cashierLogoUrl` / `cashierProductImageUrl` | 512 each |
| `brandInfo.cashierDisplayName` | 128 |

> [!NOTE]
>
> Note the asymmetry that catches people out: `successRedirectUrl` and
> `failedRedirectUrl` allow **1024**, `cancelRedirectUrl` only **512**. A URL that
> passes on two of the three fields can fail on the third.

## If a value is too long, is it truncated or rejected?

**Rejected.** Length limits are enforced on the request, before any order is
created; nothing is silently truncated, and there is no field where an
over-length value is accepted in shortened form. The failure comes back as
`A0003 Parameter validation failed` with the offending field named in the
message, so read the message rather than diffing your payload by hand.

Practical consequence: validate lengths on your side before you call, especially
for fields you build by concatenating (`orderDescription`, `merchantOrderId`,
`goodsName`) and for anything a user typed.

## `extendInfo` is only 128 characters. Can I pass more, and do I get it back?

You get it back, and you cannot make it bigger.

* **You get it back**: `extendInfo` is stored verbatim and echoed on
  `/api/v1/order/inquiry` **and** in the payment notification — both carry the
  same order structure, so whatever you put in comes back out unchanged in both.
* **128 is the limit**, and it is a request-validation limit, so 129 characters
  is an `A0003`, not a truncation.

So treat it as a **key, not a payload**: put your own order reference or a short
routing token in it and look the rest up in your own system. `merchantOrderId`
(64) is the field designed for your business order number; between the two you
have enough to key any lookup. If you need per-order structured data on the
Waffo side, the fields that carry semantics — `goodsInfo`, `riskData`,
`addressInfo` — are the intended homes for it, not `extendInfo`.

## What exactly happens if I resend the same `paymentRequestId`?

`paymentRequestId` is scoped to your merchant ID, and the check runs before any
new order can be created. Two comparisons are made — `orderAmount` and
`orderCurrency`:

| What you resend | What happens |
| - | - |
| Same id, same amount and currency | You get the **existing** order back, not a new one |
| Same id, **different** amount or currency | Rejected with `A0011 Idempotent param mismatch error` |
| Same id, at the same moment as the first call | One call wins; the other gets `A0040 Order cannot be concurrently created` |

When the existing order is returned, what you receive depends on where that
order got to: an order still in progress has its payment step retried and you
receive current payment details, and an order that already reached a final state
replays the stored result — including replaying the original failure, if it
failed. That is deliberate: a retry can never turn a settled order into a
different outcome.

Two consequences worth designing for:

* **Only amount and currency are compared.** Resending the same id with a
  different payment method, a different user or a different description does
  **not** raise a mismatch — it returns the first order. If you want a genuinely
  different order, use a new `paymentRequestId`.
* **`A0040` is a "try again", not a failure.** It means your two calls collided.
  Do not create a second order id in response; re-call with the same id, or run
  Order Inquiry on it.

## I got `A0040 Order cannot be concurrently created`. Should I retry?

Yes — with the **same** `paymentRequestId`. It is raised when two creates for the
same id are in flight at once, so exactly one of them is doing real work and the
other has been turned away to protect you from a duplicate order. Retry after a
short pause, or go straight to Order Inquiry on that id; either way you converge
on the one order that exists.

What you must not do is treat it as "the order was not created" and reissue with
a fresh id. That is how a genuine double charge gets built.

## Can I send `orderAmount` as 0?

A zero amount is not rejected by the generic amount check — the request-level rule
is that the amount must not be **negative** — but whether a zero-amount order can
actually be created depends on the payment product and the payment method you
are using, and a zero amount is meaningless to most of them.

So do not build a flow on it. If your use case is "verify the card without
charging", the mechanism designed for that is
[tokenization](/docs/en/support/tokenization-faq), not a zero-amount order. If your
use case is a free order, do not create a Waffo order at all.

Negative amounts fail with `A0003`.

## How many decimals may I send, and can an amount be rejected for its shape?

Decimal places are checked against the currency's ISO 4217 precision, and a
value with too many decimals fails with `A0003` — the full rule, including the
currencies that must carry no decimals at all and the different treatment of
cross-currency orders, is in
[Amounts and currencies](/docs/en/developer-docs/core-concepts/currency) and
[Order Amounts, Limits & Declines](/docs/en/support/order-amount-precision-and-decline-faq).

There is a second, less obvious check. Some payment method and currency
combinations accept only amounts that are a **whole multiple of a step** — for
example only multiples of ten, or only whole units with no decimals at all —
and the rule is tied to the combination, not to the currency alone. This is why
an amount that is perfectly valid in a currency can still be rejected on one
particular method. If your prices are not round numbers, test each method you
intend to offer with a realistic amount rather than a tidy one, and treat the
rejection as a configuration fact rather than a bug.

## What format does `orderRequestedAt` need, and does my clock have to match yours?

The format is ISO 8601 to **millisecond** precision, ending in a literal `Z` —
`2023-04-03T03:00:00.000Z`. It is a UTC timestamp: a numeric offset such as
`+08:00` is not an accepted substitute, and neither is second-only precision.
A value that does not parse fails with `A0003`, and the error message states the
expected format.

Your clock does not have to agree with Waffo's — `orderRequestedAt` is recorded
as *your* request time and an order is not rejected for being skewed from server
time. The timestamp that **is** compared against the clock is `orderExpiredAt` —
and the comparison is forgiving: an expiry that is already in the past is
**discarded** at create time and the order is created with the default expiry for
the payment method, so a clock bug on your side does not surface as an error. It
surfaces as an order that stays open longer than you intended. `A0034 The order
expiry time provided by merchant has expired` exists in the contract, but you
will not see it from a past `orderExpiredAt` on create. Send both timestamps in
the same UTC format and the whole class of problem disappears.

## What is `riskData` for, and does leaving it out hurt me?

`riskData` carries what you already know about the user that Waffo cannot see:
whether they are new (`userIsNew`), whether this is their first purchase
(`userIsFirstPurchase`), an individual or a business (`userType`), the IP they
registered from (`userRegistrationIp`), the IP they were last seen on
(`userLastSeenIp`), and their legal and display names.

It is optional. None of it is validated against anything, and omitting it does
not fail the order. What it does is give risk screening more to work with on a
transaction it would otherwise judge on the payment alone — which is why the
fields exist. Two rules if you send it: send **true** values, and send them
consistently, because inconsistent history data is worse than no history data.
Do not fabricate a registration IP or flag every user as returning.

The same logic applies to `userInfo.userBrowserIp` and `userInfo.userAgent`:
optional in general, but expected when you collect the card yourself and post
card, Apple Pay or Google Pay data through the API rather than using the Waffo
checkout — in that flow they are the only signal of the real browser.
`addressInfo` follows the payment method: it is optional in the contract and
becomes mandatory where the method or the scenario requires a shipping address —
see [Conditionally Required Fields](/docs/en/support/conditional-required-fields-faq).

## Do I have to send `goodsInfo`, and is `goodsUrl` or `appName` enough?

The object itself is optional, and the create request does **not** enforce the
fields inside it beyond their length limits — a `goodsInfo` with only `goodsName`
is accepted. The contract documents `goodsName` plus **either** `goodsUrl` **or**
`appName` as the expected content, because that is what compliance and risk
review look at: a web merchant sends the product URL; an app-only merchant sends
the package name, for example `com.example.app123`. They are alternatives, not
both.

Where a field inside `goodsInfo` *is* rejected as missing, the rule comes from
the payment method, not from the object: some methods require merchandise
details and report the missing path the same way as any other conditionally
required field — see
[Conditionally Required Fields](/docs/en/support/conditional-required-fields-faq).

So the practical advice is: either omit the object or send it complete. Omitting
it does not by itself get an order blocked, but it removes the merchandise
context from risk screening, which for some business categories is exactly the
context that gets a borderline transaction approved.

## Can I restrict payment to specific card BINs?

Yes, through `paymentInfo.payMethodProperties`, which is a JSON string. Two keys
are involved:

| Key | Meaning |
| - | - |
| `cardPrefix` | The permitted card prefixes (BINs). **4 to 9 digits** each, comma-separated, up to **80** prefixes |
| `cardPrefixPurpose` | Why you are restricting: `restriction` or `promotion` — this selects the message the user is shown |

Digits only, and each prefix must be within the 4-to-9 digit range; anything else
is an `A0003`. Set `cardPrefixPurpose` deliberately: a shopper who is told "this
card is not eligible for this promotion" reacts very differently from one told
"this card is not accepted", and the two wordings come from that one value.

The same properties field is where a small number of other method-specific keys
live, including the payer tax identifier some markets require — see
[Conditionally Required Fields](/docs/en/support/conditional-required-fields-faq).

## Can I decide whether a payment goes through 3DS?

You can express a decision, on card-like payments, with
`cardInfo.threeDsDecision`. It takes exactly two values:

| Value | Meaning |
| - | - |
| `3DS_FORCE` | Authenticate with 3DS |
| `NO_3DS` | Do not authenticate with 3DS |

Anything else is rejected as invalid card data. Two limits to understand before
you rely on it:

* It applies only to **card-like** payments where the **customer is present**.
  On a merchant-initiated transaction — a charge you make against a stored token
  with no customer in session — the field is not applied.
* It is a decision you express, not a guarantee about the issuer. Whether an
  authenticated payment then succeeds, and whether an unauthenticated one is
  declined by the bank, is the issuer's call.

If you want to collect card numbers on your own page at all, that is a
compliance question before it is a parameter question: you need the appropriate
card-data qualification, and the alternative — letting Waffo's checkout or the
tokenization flow handle the card — is what the
[tokenization](/docs/en/support/tokenization-faq) and
[checkout](/docs/en/developer-docs/integration/checkout/steps) integrations exist for.

## How do I charge a stored card with no customer present?

Send `paymentInfo.merchantInitiatedMode`, which takes two values:

| Value | Use for |
| - | - |
| `scheduled` | A charge on a known schedule — a renewal or an instalment |
| `unscheduled` | A charge triggered by your business at an unpredictable moment — an account top-up, a usage overage |

`userPaymentAccessToken` is **required** whenever you send
`merchantInitiatedMode`; without it the request fails validation, because there
is no stored credential to charge. Sending the mode also marks the transaction as
merchant-initiated, which is what changes how it is authenticated — and is why
`threeDsDecision` stops applying.

For how the token is created in the first place, and the difference between the
customer-present first payment and the charges that follow, see
[Tokenization](/docs/en/support/tokenization-faq).

## Can `payMethodName` carry more than one value?

Yes. Both `payMethodName` and `payMethodType` accept a **comma-separated list**,
and values are matched case-insensitively. That is how you narrow a checkout to a
subset rather than to exactly one method — for example passing both the credit
and debit entries for one card scheme so a shopper can use either.

The published parameter guidance for the common cases is worth following exactly:
for open card acceptance, pass `payMethodType="CREDITCARD,DEBITCARD"` and leave
`payMethodName` empty so the card scheme is determined from the BIN the shopper
types; to pin a single method, pass both fields; to let the shopper choose
everything, pass neither. Exact values for your account come from
`/api/v1/paymethodconfig/inquiry` or the Portal's Payin page — do not hardcode a
list copied from a specification.

## What if `merchantInfo.merchantId` does not match my API key?

It fails immediately, with `A0003`, before any order work happens: the merchant
ID in the body must be the merchant the API key you signed with belongs to. The
same check has three neighbouring failures worth telling apart:

| Situation | Code |
| - | - |
| `merchantId` does not match the API key | `A0003` |
| `merchantId` does not exist at all | `A0003` |
| `subMerchantId` does not exist under that merchant | `A0003` |
| The sub-merchant exists but is not active | `A0006 Merchant status abnormal` |

The distinction matters when you debug: `A0003` on this axis means *you sent the
wrong identifier*, while `A0006` means *the identifier is right and the account
is not usable* — the second one is not fixable in code. If you run several
merchant accounts, note that an API key belongs to one of them; there is no
cross-account key. See
[Authentication & signatures](/docs/en/support/auth-signature-faq).

## What is `userId` for?

`userId` (String 64, required) is **your** identifier for the paying user,
along with `userEmail` and `userPhone` (format `+CountryCode-Number`, for
example `+62-08990013157`). These identify the user on your side — they are
not Waffo account credentials.

## What should I pass in `userTerminal`?

`userTerminal` tells Waffo where the user actually is, so the returned
payment link works in that environment:

| Value | Use when |
| - | - |
| `WEB` | The user is on a PC browser. |
| `APP` | The user is inside your mobile app — you receive redirect links optimised for the app environment. |
| `IN_WALLET_APP` | The user is already inside a payment method's wallet app. |
| `IN_MINI_PROGRAM` | The user is inside a payment method's mini program. |

Not every payment method supports every terminal type; the value also
affects which payment methods can be offered.

## What is `productName`, and when do I use `DIRECT_PAYMENT`?

`productName` selects the payment product: `ONE_TIME_PAYMENT`,
`MINI_PROGRAM_PAYMENT`, or `DIRECT_PAYMENT`. Most merchants use
`ONE_TIME_PAYMENT`. You can check which products are enabled for your
account in the Waffo Merchant Portal under **Payin Service** — passing a
product that is not in your contract fails validation.

## Why did I get `A0003 Parameter validation failed`?

The most common causes are field length and format limits. Ones that
frequently trip merchants up:

* `cancelRedirectUrl` — maximum length **512**, half of what the success and
  failure redirect URLs allow
* `goodsInfo.goodsUrl` — maximum length **128**, must be a full URL
  (expected for compliance and risk review)
* `userPhone` — must follow `+CountryCode-Number`
* `orderRequestedAt` — must be ISO 8601 with milliseconds and a `Z` suffix

The error response names the offending field; check it against
[the field table above](#is-there-one-table-showing-which-fields-are-mandatory-and-how-long-they-can-be).

One format failure that is **not** `A0003`: a `userEmail` that does not look
like an address comes back as `B0007 Invalid email format`. If you see `B0007`,
the fix is the email value, not a length or timestamp.

## `userEmail` is mandatory but I never collected one. What do I send?

Send a **unique** synthetic address in the published fallback form
`userId@examples.com`, substituting the user's actual id. Format is validated —
the value has to look like an address, otherwise the create fails with
`B0007 Invalid email format` — but uniqueness is not enforced by the API, which
is precisely why it is on you to keep it unique.

The two things not to do, because both damage you rather than failing loudly:

* **Do not send a literal placeholder** such as `test@…`.
* **Do not share one address across users.** One address on many payments is
  exactly the pattern risk screening is built to notice, so the cost lands as
  declines on good transactions, not as an error on the create call.

Once you do collect real addresses, send the real ones. If your historical
traffic needs the fallback applied retroactively, that is a support request
requiring your authorization, not a parameter change.

## When do I need to send `userCurrency`?

`userCurrency` is optional, and there is no "userCurrency is required" error. It
declares the currency the user will pay in, for the case where that is not your
`orderCurrency`.

What it actually affects is **which of your contracts can serve the order**. A
cross-currency order can only be routed through a contract configured for that
currency pair; if you leave the field out and no same-currency contract matches
either, nothing is left to route to and you receive
`A0018 Payment method in merchant order does not match with current merchant contract` —
which reads as a payment-method problem but is really a currency-pairing problem.
That is the single most misread failure on this endpoint.

The published rule for a cross-currency order is to send `payMethodCountry` when
you have not pinned `payMethodType` or `payMethodName`; `userCurrency` itself
stays optional. Which currencies your contracts cover, and how the three
currencies on an order relate, are in
[Which Amount and Currency Fields Do I Check?](/docs/en/support/order-amount-and-currency-fields-faq).

## Why did I get `A0018 Payment method does not match`?

`A0018` means the payment method in the order does not match your current
merchant contract. Which payment methods are available depends on what is
enabled for your account — check the Merchant Portal, or contact integration
support to adjust your contract.

## The payment failed — how does the user retry with a different payment method?

Create a **new order with a new `paymentRequestId`**. Reusing the previous
`paymentRequestId` with a changed `payMethod` is an idempotency mismatch and
fails with `A0011`.

One extra rule when switching away from cards: a saved card token
(`userPaymentAccessToken`) is only valid for card payments. If the failed
attempt was a card payment and the retry specifies a non-card method (a
bank transfer or wallet, for example), **do not carry the card token over**
— the combination cannot be routed and order creation fails with a
"no available channel" error.

## Which WeChat Pay method and order currency should I pass?

WeChat Pay is not a single configuration. `WECHATPAY` exists as three regional
configurations, each tied to one order currency, and `WECHATPAYHK` is a
separate method:

| Method | Order currency | Wallets that can pay |
| - | - | - |
| `WECHATPAY` — HKG | `HKD` | Wallets registered in Hong Kong **and** in Mainland China |
| `WECHATPAY` — CHN | `CNY` | Mainland China wallets only |
| `WECHATPAY` — USA | `USD` | Mainland China wallets only |
| `WECHATPAYHK` | `HKD` | Hong Kong wallets only |

Pick the combination that matches where your users' wallets are registered — if
the wallet is not covered by the configuration you charged in, the user cannot
complete the payment. Confirm which of these are enabled for your account with
`/api/v1/paymethodconfig/inquiry`; each entry carries the method's `country`
and `currency`.

## Error code families

| Prefix | Family |
| - | - |
| `Axxxxx` | Merchant-related (credentials, parameters, contract, limits) |
| `Bxxxxx` | User-related |
| `Cxxxxx` | System-related |
| `Dxxxxx` | Risk-related rejection |
| `Exxxxx` | Unknown — retry the transaction (or Order Inquiry) until a final Success / Failed status is returned |


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