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

# Per-Card Purchase Limit API

> How to cap how many units of a product or product category a single card may buy, using the signed transactionlimit create / update / inquiry endpoints: request fields, the two mutually exclusive limit groups, idempotency, effective window, and what the shopper sees when a rule is hit.

> [!WARNING]
>
> This capability has to be switched on for your account before the endpoints will
> answer. If a call comes back with a permission error, contact your Waffo
> integration contact rather than debugging your request.

If you sell digital goods, game currency, event tickets or anything else that
resellers like to buy in bulk, you can cap **how many units one payment
instrument may buy** — by product, or by product category. Waffo enforces the
cap at payment time and declines the payment when it is exceeded.

This page covers the three signed endpoints that let you manage those rules from
your own systems. It is not about amount ceilings: for the minimum/maximum order
amount on a payment method, and for the per-account single-transaction and daily
amount limits, see
[Limits, Currency & Configuration Errors](/docs/en/support/error-codes-and-limits-supplement).
Signing, keys and headers work exactly as they do for every other API — see
[Authentication & Signature](/docs/en/support/auth-signature-faq).

## What exactly can I limit, and what happens when a rule is hit?

You register a **maximum quantity per payment instrument** for either:

* one specific product — you supply the product identifier and the maximum
  quantity allowed for it, or
* one product category — you supply the category and the maximum quantity
  allowed across everything in it.

From then on, when you create an order you pass the goods information and the
quantity being bought. If the order would push that instrument past the
registered maximum, the payment is **declined** — the shopper cannot complete
it. The order does not silently succeed with a reduced quantity, and there is no
partial acceptance.

Two consequences worth designing for:

* The rule is a **hard decline**, not a review queue. If you want a softer
  outcome (a warning, a manual review, a per-customer allowance), you have to
  build that on your side before calling order creation.
* A declined attempt is a normal risk decline on the order. Show the shopper a
  neutral message and do not retry the same order automatically — retrying will
  hit the same rule.

## Which endpoints exist, and what can each one do?

| Endpoint | What it does |
| - | - |
| `POST /api/v1/risk/transactionlimit/create` | Registers a new rule |
| `POST /api/v1/risk/transactionlimit/update` | Changes an existing rule, or terminates it |
| `POST /api/v1/risk/transactionlimit/inquiry` | Reads a rule back |

All three are signed endpoints on the same host, with the same headers and the
same signing algorithm as order creation. They sit in the **payin** scope of an
API key, so a key issued for withdrawals or payouts alone cannot call them.

## What do I send on create?

| Field | Notes |
| - | - |
| `merchantInfo.merchantId` | Required. Must be the same merchant ID your API key is issued for — see the next question. |
| `merchantInfo.subMerchantId` | Optional. When present it must be a sub-merchant that belongs to that merchant ID. |
| `merchantRequestId` | Required. Your own reference for this rule, and the idempotency key. |
| `goodsInfoLimitConfig.goodsId` + `maxGoodsQuantity` | The product-level group. Send both together. |
| `goodsInfoLimitConfig.goodsCategory` + `maxCategoryQuantity` | The category-level group. Send both together. |
| `startTime` | Required. When the rule starts applying, ISO 8601 (for example `2026-09-01T03:00:00.000Z`). |
| `endTime` | When the rule stops applying, same format. |
| `extendInfo` | Optional passthrough, JSON string. |

Both maximum-quantity values must be **1 or greater**. A maximum of zero is not
a way to block a product outright — if you want a product unavailable, do not
offer it.

## Can I set a product limit and a category limit in the same request?

No. The two groups are **mutually exclusive per request**:

* `goodsId` and `maxGoodsQuantity` must be provided together.
* `goodsCategory` and `maxCategoryQuantity` must be provided together.
* Only **one** of those two groups may appear in a single create call.

If you want both a per-product cap and a per-category cap, create two rules.

## Why do I get a parameter error mentioning my merchant ID?

Because the `merchantId` in the body did not match the merchant ID that your API
key is issued for. All three endpoints assert that the two agree before doing
anything else, and reject the call as `A0003` **Parameter validation failed**
with the detail `mid in params is inconsistent with apiKey`.

The same check applies to `subMerchantId`: if you send one, it must exist and
belong to the merchant ID in the request. Otherwise you get `A0003` with the
detail `subMerchantId is invalid or does not belong to the provided merchantId`.

This is the single most common integration mistake here, and it is almost always
one of two things: a key from a different account (sandbox key against
production, or a sub-account's key), or a hardcoded merchant ID left over from
a copied sample.

## What comes back, and what does `ruleStatus` mean?

Create and update both answer with the identifiers plus the rule's status:

| Field | Notes |
| - | - |
| `ruleId` | Waffo's identifier for the rule. Store it — it is the stable handle. |
| `merchantRequestId` | Echoed back, so you can match the answer to your request. |
| `ruleStatus` | `ACTIVE` — the rule exists and will be evaluated inside its effective window. `TERMINATED` — the rule no longer applies. |

Note that `ACTIVE` describes the rule's own lifecycle, not whether it is
currently in force: a rule with a future `startTime` is `ACTIVE` and simply has
not started yet.

## How does idempotency work if my create call times out?

`merchantRequestId` is the idempotency key. Resend the identical request with
the same `merchantRequestId` and you get the existing rule back rather than a
second one — which is what you want after a timeout, since you cannot tell from
a timeout whether the rule was created.

Two rules follow from that:

* **Generate one `merchantRequestId` per intended rule**, not per attempt. If
  you generate a fresh one on retry, you will end up with two rules.
* **Do not reuse a `merchantRequestId` for a different rule.** Reusing an
  idempotency key with different contents is what `A0011` **Idempotent param
  mismatch error** is for.

## How do I change or stop a rule?

Call `update` and identify the rule by **either** `merchantRequestId` **or**
`ruleId` — one of the two is required, and `ruleId` is the safer choice because
it is unambiguous.

On update, the limit fields are optional and follow the same pairing rule as on
create: send `maxGoodsQuantity` whenever you send `goodsId`, and
`maxCategoryQuantity` whenever you send `goodsCategory`. You can also move
`startTime` and `endTime`.

To stop a rule, set `ruleStatus` to `TERMINATED`. There is no delete: a
terminated rule stays readable through `inquiry`, which is useful when you need
to explain a historical decline.

## How do I read a rule back?

Call `inquiry` with `merchantInfo` plus **either** `merchantRequestId` **or**
`ruleId`. The answer restates the rule as Waffo holds it — identifiers,
`ruleStatus`, the limit configuration, and the effective window.

Use it for three things:

* **Reconciling after a timeout**, before deciding whether to create or update.
* **Verifying what is actually live** before a sale, rather than trusting your
  own copy of the configuration.
* **Explaining a decline** to your support team by showing the rule that was in
  force.

## Do I have to change anything about my order creation?

Yes — a rule can only bite on information you actually send. Registering a rule
does nothing on its own: at order creation you must pass the goods identifier
and/or the goods category the rule is registered against, together with the
quantity being bought. An order that carries no goods information cannot be
matched against a quantity rule.

Two practical notes:

* **Use the same identifiers on both sides.** The value you register as
  `goodsId` has to be the value you send at order creation, character for
  character. Mismatched casing or a different internal SKU scheme is the usual
  reason a rule "does not work".
* **Category rules need a category on the order.** If your catalogue does not
  carry a category concept today, a per-product rule is the cheaper starting
  point.

Field-level details for the goods fields are in the
[Order Creation Field Guide](/docs/en/support/order-create-field-guide).

## Can I list all my rules in one call?

No. `inquiry` is a lookup by identifier — `ruleId` or `merchantRequestId` — not
a paged list of everything you have registered. So keep your own register of the
rules you created, storing the `ruleId` next to the product or category it
covers.

That register is what you audit against before a launch: read each rule back
through `inquiry` and compare, rather than assuming your configuration store and
Waffo agree.

## How quickly does a change take effect?

A rule is evaluated against its own effective window, so:

* A rule created with a `startTime` in the past is in force for the next order.
* A rule created with a future `startTime` does nothing until then, which is the
  clean way to arm a cap ahead of a launch or a flash sale.
* Terminating a rule stops it applying; payments that already succeeded are
  unaffected.

One consequence people miss: **an order you created before arming the rule is
still evaluated against it** if the shopper pays afterwards. Screening happens
when the payment is actually attempted, not when the order was created, so an
order sitting on a checkout page when you arm a cap will decline on payment.
Only payments that already succeeded are out of reach of a new rule.

Do not schedule a rule to start at the exact second a sale opens. Arm it
comfortably before, and verify with `inquiry` that it reads back as you expect.

## Which error does the shopper's payment get when my rule fires?

A **risk decline**, not a rule-specific code: the payment fails with `D0001`
(risk rejection) or `D0002` (risk transaction limit), HTTP `406`, exactly as any
other risk decision would. There is no code that says "the merchant's own rule
declined this".

Two things follow:

* **Keep your own record of what you restricted.** On order inquiry and on the
  payment notification the failure is reported under a generic rejection
  category, so you cannot attribute a decline to your own rule after the fact.
* **Tell the shopper something useful.** Since the code does not distinguish a
  cap you set from any other decline, the wording in your interface has to come
  from your own knowledge of the rule, not from the response.

See [Risk Declines & 3D Secure](/docs/en/support/risk-declines-and-3ds-faq) for how
the two codes behave on each surface.

## Which errors should I handle explicitly?

These are the codes worth branching on. The general families are documented in
[Common Errors Troubleshooting](/docs/en/support/errors-troubleshooting-guide).

| Code | Meaning | What to do |
| - | - | - |
| `A0001` | Invalid API secret key | Wrong or revoked key, or the wrong environment. |
| `A0002` | Invalid signature | Signing problem — see the signature FAQ. |
| `A0003` | Parameter validation failed | The detail text names the field. Covers the merchant-ID mismatch, the sub-merchant check and the field-pairing rules. |
| `A0004` | Permission denied | Your key is not scoped for this call, or the capability is not enabled on the account. |
| `A0011` | Idempotent param mismatch | You reused a `merchantRequestId` with different content. |
| `C0001` | System error | Retry with the same `merchantRequestId`; idempotency protects you. |

On the **payment** side, an order that trips a registered rule is declined as a
risk rejection, not as a parameter error. Treat it as final for that order.

## Does a rule apply to sub-merchants?

A rule is registered against the merchant ID in the request, optionally narrowed
to one sub-merchant. If you operate several sub-merchants and want the same cap
everywhere, register the rule per sub-merchant — a rule scoped to one
sub-merchant does not cover the others, and the endpoints validate that the
sub-merchant you name really belongs to you.

If you are new to the sub-merchant model, start with
[Multi-brand & sub-merchant setups](/docs/en/support/multi-brand-and-sub-merchant-faq).

## How is this different from the amount limits I already have?

They are unrelated checks that fail differently, which matters when you are
diagnosing a decline:

| Check | What it measures | Where it is configured |
| - | - | - |
| Per-card purchase limit (this page) | **Quantity** of a product or category bought on one payment instrument | By you, through these endpoints |
| Payment-method amount range | Order **amount** against the minimum/maximum for that payment method and currency | Contract configuration |
| Account single-transaction and daily limits | Order amount, and the day's cumulative amount, per order currency | Account configuration |

A quantity rule will not stop a single very large order, and an amount limit will
not stop a hundred small ones. If bulk-buying is your problem, you need the
quantity rule.

## Where do I look next?

* [Order Creation Field Guide](/docs/en/support/order-create-field-guide) — the goods
  fields you pass on order creation.
* [Limits, Currency & Configuration Errors](/docs/en/support/error-codes-and-limits-supplement)
  — the amount-based ceilings and which code means which.
* [Confirming Payment Results](/docs/en/support/payment-results-faq) — how a declined
  payment is reported to you.

Still unclear whether a decline came from one of your own rules? [Contact
support](mailto:support@waffo.com) with the order ID and the `ruleId`.


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