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

# Mini-Program Payments

> How to take payments inside a wallet mini program: which front-end JS APIs and which Waffo endpoints you must integrate, what to send on order creation, how refunds work, how to identify the user with userid/inquiry, and why you only get the user's public ID unless extra scopes are approved.

Some wallets host third-party **mini programs** — small apps that run inside the
wallet's own app. If you publish one, you can collect payment inside it without
sending the user out to a browser: your mini program asks the wallet to pay, and
your backend talks to Waffo exactly as it does for any other order.

This page covers the whole integration: the payment path, the refund path, and
the separate user-identity path. Signing, keys and headers are unchanged from
every other Waffo API — see
[Authentication & Signature](/docs/en/support/auth-signature-faq). Order
fields not specific to mini programs are in the
[Order Creation Field Guide](/docs/en/support/order-create-field-guide).

> [!NOTE]
>
> Publishing a mini program requires a developer account on the wallet's own
> mini-program platform, and Waffo assigns you the app identifier you send on API
> calls. Ask your Waffo integration contact to start that process before you write
> any code — the identifiers are not self-service.

## What do I actually have to build?

Two independent tracks. Payment is mandatory; identity is mandatory only in its
first step.

**Payment track**

| Piece | Where | Required |
| - | - | - |
| `my.tradePay` (wallet JS API) | Your mini program's front end | Yes |
| `POST /api/v1/order/create` | Your backend | Yes |
| `POST /api/v1/order/inquiry` | Your backend | Yes |
| Payment result notification (webhook) | Your backend | Yes |
| `POST /api/v1/order/refund` | Your backend | Only if you refund |
| `POST /api/v1/refund/inquiry` | Your backend | One of these two |
| Refund result notification (webhook) | Your backend | One of these two |

**Identity track**

| Piece | Where | Required |
| - | - | - |
| `my.getSystemInfo` (wallet JS API) | Front end | Yes |
| `my.getAuthCode` (wallet JS API) | Front end | Yes |
| `POST /api/v1/userid/inquiry` | Your backend | Yes |
| `POST /api/v1/userinfo/inquiry` | Your backend | Only if you need profile data |

Both identity endpoints sit in the **payin** scope of an API key, so the same
key you use for order creation can call them.

## How does the payment flow run end to end?

1. The user taps *Pay* in your mini program. Your front end calls **your own**
   backend, not Waffo directly — your API key and your signing private key must
   never reach the front end.
2. Your backend calls `/api/v1/order/create` with the mini-program fields below,
   and receives a payment URL.
3. Your front end passes that URL to `my.tradePay`.
4. The wallet runs its own payment UI and returns success or failure to your
   front end.
5. Waffo sends the authoritative result to your `notifyUrl`. Your backend
   fulfils the order on that, not on step 4.

That last point is the one that causes real incidents. The value
`my.tradePay` hands back is a **UI outcome**: it tells you what happened in the
user's app, not what happened to the money. A user who backgrounds the wallet,
loses signal, or force-closes the app can leave you with no front-end result at
all on a payment that succeeded. Treat the webhook — or an
`/api/v1/order/inquiry` poll — as the only source of truth, exactly as described
in [Confirming Payment Results](/docs/en/support/payment-results-faq).

## What do I send on order creation?

Everything a normal order needs, plus three mini-program specifics:

| Field | Value |
| - | - |
| `paymentInfo.productName` | `MINI_PROGRAM_PAYMENT` |
| `paymentInfo.payMethodName` | The wallet's payment-method name, as it appears in your payment-method configuration |
| `paymentInfo.payMethodProperties.appId` | The mini-program app identifier Waffo assigned you |
| `userInfo.userTerminal` | `IN_MINI_PROGRAM` |

`userTerminal` matters more than it looks: it is part of how Waffo decides
whether a payment method can serve this particular request. A mini-program order
submitted as `WEB` can be rejected as having no usable payment method, because
not every method supports every terminal. The other accepted values are `WEB`,
`APP` and `IN_WALLET_APP` (the user is inside the wallet app but not inside a
mini program).

If you are unsure which payment-method name and which currency your account is
configured for, read it back from `/api/v1/paymethodconfig/inquiry` — see
[Payment Method Configuration & Order Limits](/docs/en/support/payment-method-config-and-limits-faq).

## How do I refund a mini-program payment?

Through the ordinary refund API — `/api/v1/order/refund`, with
`/api/v1/refund/inquiry` or the refund webhook to follow the outcome. There is
no mini-program-specific refund endpoint, and no mini-program-specific rule
about how many refunds an order may take.

What does vary is the **payment method's** refund capability: whether partial
refunds are allowed at all, and whether they can be repeated. Read
`refundAllowed` from the payment-method configuration rather than assuming, and
see [Payment Method Refund Limits](/docs/en/support/payment-method-refund-limits-faq)
for what each capability means in practice.

## How do I identify the user inside my mini program?

Your mini program cannot see who the user is by itself; the wallet mediates it.

1. Front end calls `my.getAuthCode` with the scopes you need. It returns a
   short-lived `authCode`.
2. Front end sends that `authCode` to your backend.
3. Your backend calls `/api/v1/userid/inquiry` with `merchantId`, `appId`, the
   `authCode`, and `appPlatform` (the wallet-platform code Waffo assigned you).
4. You get back:

| Field | Notes |
| - | - |
| `userId` | The user's **public identifier** on that wallet — stable for that user in your mini program |
| `accessToken` | Needed only if you go on to request profile data |
| `accessTokenExpiryTime` | ISO 8601. The token is not indefinite; do not cache it past this |

Use `userId` as the key for your own account linking. Do **not** treat it as a
phone number, an email address, or a cross-wallet identity — it is an opaque
identifier scoped to your mini program.

## Can I get the user's email address so I can deliver the goods?

Not by default, and this is the single most common surprise in this integration.

`my.getAuthCode` **must** include the scopes `MINI_PROGRAM` and `PUBLIC_ID`.
Those two get you the public user identifier and nothing else. Everything else
is an **optional scope that has to be approved per merchant** by the wallet
platform:

| Optional scope | What it unlocks |
| - | - |
| `USER_CONTACTINFO_EMAIL` | Email address |
| `USER_CONTACTINFO` | Contact details |
| `USER_ADDRESS` | Address |
| `USER_BIRTHDAY` | Date of birth |
| `USER_GENDER` | Gender |
| `USER_LOGIN_ID` | The user's login identifier on the wallet |
| `USER_NAME` | Name |

By default **no personal information beyond the public ID is granted**. To get
any of the above, you have to request the specific scope and provide a written
justification for why your mini program cannot function without it. Approval is
the wallet platform's decision, not Waffo's, and it is granted scope by scope.

Design accordingly: build the flow so that a user with only a public ID can
still complete a purchase and receive what they bought. In practice that means
collecting a delivery email in your own mini-program UI, and treating any
profile field the wallet grants you as a convenience that pre-fills a form —
never as a required input.

## What does the profile lookup return?

`POST /api/v1/userinfo/inquiry` takes `merchantId`, `appId`, `appPlatform` and
the `accessToken` from the identity step, and returns whatever your approved
scopes cover:

| Field | Notes |
| - | - |
| `userName` | Full name |
| `nickName` | Display name |
| `gender` | A single-letter code (`M` / `F`) |
| `birthday` | A formatted date string — parse defensively, do not assume a separator |
| `contactEmail` | Email address |
| `contactMobileNumber` | Mobile number, including country code |
| `addressInfo[]` | `address`, `city`, `region`, `postcode`, `addressCountryCode` |
| `userLoginId` | The user's login identifier on the wallet |
| `kyc` | The user's verification tier, as a two-digit code: no verification, partial, or full |

Every one of these is **conditional**. A field you were not granted simply does
not arrive, and a field you were granted can still be absent because the user
never filled it in. Write your mapping so that a missing field is a normal case,
not an error.

## What do the identity error codes mean?

| Code | Meaning | What to do |
| - | - | - |
| `A0001` | Invalid API secret key | Wrong key or wrong environment. |
| `A0002` | Invalid signature | See the signature FAQ. |
| `A0003` | Parameter validation failed | The detail names the field. A mismatch between the body's merchant ID and your API key's merchant ID lands here too. |
| `A0006` | Merchant status abnormal | Account-level problem; contact support. |
| `A0010` | Operation not allowed in merchant contract | Your account is not configured for this. |
| `B0008` | Auth code is invalid | Get a fresh code from `my.getAuthCode`. |
| `B0009` | Auth code is already expired | Codes are short-lived — exchange them immediately, never store them. |
| `B0010` | Auth code has been used | Each code is single-use. Do not retry the exchange with the same code. |
| `B0011` | Access token is invalid | Re-run the identity step from `my.getAuthCode`. |
| `B0012` | Access token is expired | Re-run the identity step; honour `accessTokenExpiryTime`. |
| `C0001` | System error | Retry. |

The three `B0008`–`B0010` codes are all "your auth code is no longer usable".
The correct handling for every one of them is the same: go back to the front end
for a new code, rather than retrying the backend call.

Payment errors are the ordinary ones — see
[Common Errors Troubleshooting](/docs/en/support/errors-troubleshooting-guide).

## Can I run subscriptions inside a mini program?

Yes, as a distinct product: `MINI_PROGRAM_SUBSCRIPTION` rather than
`MINI_PROGRAM_PAYMENT`. It carries one extra requirement — a **user access
token** obtained through the identity flow above must be supplied when the
subscription is created, because the wallet needs to know which of its users is
authorising the recurring charge.

Everything else about subscriptions is unchanged, including amount rules,
notifications and cancellation. Start from
[Subscription FAQ](/docs/en/support/subscription-faq), and check what your
payment method supports before promising features to your users.

## What is different from a normal hosted-checkout integration?

| | Hosted checkout | Mini program |
| - | - | - |
| Where the user pays | The Waffo checkout page in a browser | Inside the wallet app, driven by `my.tradePay` |
| Who renders the payment UI | Waffo | The wallet |
| How you learn the result | Redirect (cosmetic) + webhook (authoritative) | JS callback (cosmetic) + webhook (authoritative) |
| Who the user is | Whatever you collect | The wallet's public user ID, plus approved scopes |
| Checkout customisation | Portal settings apply | Not applicable — there is no Waffo page |

The important consequence: **none of the checkout-page settings apply**. Display
language, method ordering, branding and the result page all live in the wallet's
UI, so your mini program owns the experience. Everything in
[Checkout display rules](/docs/en/support/cashier-display-rules-faq) and the checkout
customisation pages is irrelevant to this path.

## Do I still get the same reporting?

Yes. Mini-program orders appear in the Portal transaction list, in exports and in
settlement like any other order, and are distinguishable by their product. Order
identifiers behave identically, so your reconciliation does not need a special
case — see [Settlement Reports & Exports](/docs/en/support/settlement-reports-faq).

## Is there a Waffo SDK or JS package for the mini program?

No. There is nothing for you to install on the mini-program side. The two halves
of the integration are:

| Side | What you use |
| - | - |
| Your mini program's front end | The **wallet's own** JS APIs — `my.getSystemInfo`, `my.getAuthCode`, `my.tradePay`. They come from the wallet runtime, not from Waffo. |
| Your backend | Waffo's ordinary REST endpoints, signed the ordinary way |

Waffo's published SDKs are **server-side** helpers for calling the REST API; they
are not mini-program packages and there is no front-end bundle to embed. So
"which Waffo library do I import into the mini program" has no answer — the
correct answer is none, and your front end never talks to Waffo directly.

## What is `appPlatform` for, and what happens if I leave it out?

`appPlatform` tells Waffo **which wallet platform the auth code came from**. It is
part of the documented request contract for both identity endpoints, and the
accepted values are assigned to you by Waffo along with your `appId`.

Always send it. It is the field that disambiguates the exchange when your account
is set up on more than one wallet platform, and omitting it is not something to
rely on — there is no defined fallback behaviour for you to depend on, and the
value costs you nothing to store next to the `appId` you already hold.

## What does `B0013` mean, and how is it different from `B0011` and `B0012`?

| Code | Meaning |
| - | - |
| `B0011` | Access token is invalid — the value is not a token Waffo can resolve |
| `B0012` | Access token is expired — it was valid and its lifetime has passed |
| `B0013` | Access token is not exist — no token is on record for this request |

For your integration all three collapse to the same recovery: **go back to the
front end, call `my.getAuthCode` for a new code, exchange it for a new token.**
Retrying the backend call with the same token cannot succeed for any of them.

Do not build separate branches for the three. Build one "re-run the identity
flow" path and route `B0011`, `B0012` and `B0013` into it, exactly as you do for
`B0008`–`B0010` on the auth code.

## The token has not reached `accessTokenExpiryTime`, but creating a subscription says the token expired

Because the **wallet**, not Waffo, is the authority on whether the token is still
good at the moment of the charge. `accessTokenExpiryTime` is the latest the token
could be valid — it is not a guarantee it still is. A token can stop working
early when the customer revokes the authorisation, signs out, or the wallet
invalidates the session on its own.

When that happens the wallet's verdict reaches you as `B0009` (auth code already
expired) or `B0008` (auth code invalid) on the create call, rather than from the
identity endpoint. Both are classified as **not retryable**: the same call with
the same credential will keep failing.

The design rule that follows: **fetch a fresh token as part of the flow that
creates the order or the subscription, rather than reusing a stored one.** Caching
a token and treating `accessTokenExpiryTime` as a validity promise is the single
most common cause of this error.

## Can I run subscriptions in the mini program, and what exactly do I send?

Yes, as the `MINI_PROGRAM_SUBSCRIPTION` product. Alongside the normal
subscription fields you send the customer's access token:

| Field | Value |
| - | - |
| `paymentInfo.productName` | `MINI_PROGRAM_SUBSCRIPTION` |
| `paymentInfo.payMethodUserAccessToken` | The `accessToken` from `/api/v1/userid/inquiry` |

The token identifies which of the wallet's users is authorising the recurring
mandate, so the wallet can bind the mandate to the right account. The same field
exists on `/api/v1/subscription/change`, so a plan change in a mini program needs
a freshly obtained token too.

Everything else about subscriptions is unchanged — cycles, trials, amounts,
statuses, retries, notifications and cancellation all behave exactly as on
[Subscription creation & validation](/docs/en/support/subscription-creation-validation-faq)
and the pages it links to.

## Do the later renewals need a new token each time?

No. The token is supplied **once, at signing time**, to establish the mandate.
The stored mandate is what later periods are charged against, so you do not
re-run the identity flow on every renewal — renewals are background charges with
no customer present and no front end involved.

You need a fresh token when the customer is in front of you authorising
something new: creating a subscription, or a plan change. Not for period 2
onwards.

If a renewal fails because the customer revoked the mandate in their wallet, that
arrives as a cancellation on the payment-method side rather than as a token
error — see the wallet-revocation behaviour in
[Subscription FAQ](/docs/en/support/subscription-faq).

## Can I send the customer back to a specific page in my mini program after paying?

That is your front end's job, not Waffo's. Because there is no Waffo-hosted page
anywhere in this flow, the redirect URLs on order creation have nothing to act on
here — navigation stays inside the wallet, and control returns to the page that
called `my.tradePay` when the wallet's payment UI closes.

So: resume on the page you left, and decide what to show from **your backend's**
view of the order, which is driven by the webhook. Do not render a success screen
purely on the JS callback, for the reasons in
[Confirming Payment Results](/docs/en/support/payment-results-faq).

## Where in the Portal can I see which mini program a payment came from?

On the **individual payment's detail page**, which carries a mini-program block
with the **App ID** and **App Name** for orders that have one. Open the payment
from **Transactions → Payments** and the block is there alongside the rest of the
order.

What is **not** available: there is no mini-program filter or App ID column on
the payments **list**, so you cannot slice the list by mini program. If you run
more than one mini program and need per-app reporting, key it off your own order
records — you know which `appId` you sent on each `/api/v1/order/create` — rather
than expecting the Portal to group it for you.

Mini-program products are named in transaction exports, so the traffic is
identifiable as mini-program traffic; it is the *which* mini program that is
detail-page only.

## Is the customer the same person as on my web checkout?

Not automatically, and you should not try to make Waffo answer this. The `userId`
you get back is the wallet's **public identifier scoped to your mini program** —
it is not an email address, not a phone number, and not a cross-channel identity.

That means:

* You cannot match a mini-program shopper to a web-checkout shopper by comparing
  identifiers. There is nothing shared to compare.
* If you publish two mini programs, do not assume the identifier for the same
  human is the same in both. Design as though it is not.
* The workable pattern is your own account system: the customer signs in to your
  account inside the mini program, you store the wallet `userId` against that
  account once, and from then on your account ID is the identity that spans
  channels.

Where you have been granted a contact scope, an email address can help you
*offer* a match to the customer — but treat it as a hint to confirm with them,
never as proof that two records are the same person.

## Where do I look next?

* [Confirming Payment Results](/docs/en/support/payment-results-faq) — why the
  webhook, not the JS callback, is your source of truth.
* [Order Creation Field Guide](/docs/en/support/order-create-field-guide) — the rest
  of the order-create contract.
* [Payment Method Configuration & Order Limits](/docs/en/support/payment-method-config-and-limits-faq)
  — reading back which methods, currencies and amount ranges you have.

Stuck on the app identifier or a scope approval? [Contact
support](mailto:support@waffo.com) — both are provisioning steps, not code
problems.


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