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

# API Credential Scope & Auth Error Triage

> Which API key is allowed to call which endpoint, why Withdraw must be a separate credential, the exact order in which Waffo checks your API key, scope, signature and IP, how to read A0001 / A0002 / A0004 / A0046, and how long a credential change takes to apply.

Three pages already cover the mechanics of authentication:

| Question | Page |
| - | - |
| How do I sign a request, verify a response and a webhook, and generate a key pair? | [Authentication & Signature FAQ](/docs/en/support/auth-signature-faq) |
| How do I submit or rotate a production public key, and what is an API operation type? | [Production Public Keys & API Operation Types](/docs/en/support/production-api-key-and-public-key-faq) |
| How do I set notification URLs, turn on the IP allowlist, and use two-step verification? | [Portal Self-Service Security Configuration](/docs/en/support/portal-security-config-faq) |

**This page is the scope matrix and the triage tree** — which credential may call
which endpoint, and how to read the error when one may not.

## How many keys are involved, and who holds each one?

Four things, and only two of them are yours to keep secret:

| Item | Who generates it | Who holds it | Purpose |
| - | - | - | - |
| API key | Waffo, alongside each accepted public key | You send it on every request | Identifies which credential — and therefore which merchant and which scope — a request is using |
| Your private key | **You** | You, server-side only | Signs your requests and your webhook responses |
| Your public key | You (submitted to Waffo) | Waffo | Verifies your signatures |
| Waffo public key | Waffo | You copy it from the Portal | Verifies Waffo's responses and webhook signatures |

The relationship that matters operationally: **one API key maps to exactly one
public key.** The `X-API-KEY` you send decides which public key your signature is
checked against — so an API key and a private key are a matched pair, and
swapping one without the other fails.

## Why does my Integration page show several API keys and public keys? Which one do I use?

Because a credential is scoped to a class of operation, and you hold one per
class you use. The row you want is the one whose **API Operation Type** matches
the call you are making.

Which class covers which endpoint, exactly:

| Operation type | Endpoints it may call |
| - | - |
| **Payin** | `/api/v1/order/create`, `/api/v1/order/inquiry`, `/api/v1/order/cancel`, `/api/v1/order/refund`, `/api/v1/order/capture`, `/api/v1/refund/inquiry`, `/api/v1/subscription/create`, `/api/v1/subscription/inquiry`, `/api/v1/subscription/cancel`, `/api/v1/subscription/manage`, `/api/v1/subscription/change`, `/api/v1/subscription/change/inquiry`, `/api/v1/subscription/update`, `/api/v1/tokenization/generate`, `/api/v1/tokenization/inquiry`, `/api/v1/tokenization/remove`, `/api/v1/merchantconfig/inquiry`, `/api/v1/paymethodconfig/inquiry`, `/api/v1/saved-payment-methods/config`, `/api/v1/userid/inquiry`, `/api/v1/userinfo/inquiry`, `/api/v1/wallet/inquiry`, `/api/v1/chargeback/inquiry`, `/api/v1/chargeback/list`, `/api/v1/chargeback/update`, `/api/v1/chargeback/accept`, `/api/v1/fraud-report/list`, `/api/v1/risk/transactionlimit/create`, `/api/v1/risk/transactionlimit/update`, `/api/v1/risk/transactionlimit/inquiry` |
| **Payout** | `/api/v1/payout/create`, `/api/v1/payout/inquiry`, `/api/v1/account/balance/inquiry` |
| **Payin and payout** | Everything in both rows above |
| **Withdraw** | `/api/v1/withdraw/consult`, `/api/v1/withdraw/create`, `/api/v1/withdraw/inquiry`, `/api/v1/account/balance/inquiry`, `/api/v1/account/history/inquiry` |
| **Card collection** | `/cardcenter/web/v1/tokenization/submit` — the browser-side card submission endpoint |

Two details this table settles:

* **Balance inquiry is in both the Payout and the Withdraw scope.** It is the one
  endpoint you can reach with either credential, which is why a balance check
  never forces you to add a scope.
* **Account history is Withdraw-only.** A payout credential cannot read it.

## I used my payin key on a payout endpoint and got a permission error. Why?

Because scope is enforced per request path, not per merchant. The credential was
valid — it simply is not allowed to call that endpoint, and the request is
rejected with **`A0004` Permission denied** (HTTP 401).

The distinction to keep in mind while debugging: `A0004` means "this key, wrong
endpoint". It does not mean your key is wrong, your signature is wrong, or your
account lacks the product. Look up the endpoint in the table above, then use the
credential row whose operation type contains it.

## Why can't Withdraw be combined with Payin or Payout? I need both.

Because withdrawal moves **your own settled funds out**, where payin and payout
move a customer's money in or pay a third party. Keeping withdrawal on its own
credential means a key held by your checkout service or your payout service can
never be used to move your balance out, and that compromising one does not imply
the other.

The Portal enforces this rather than warning about it: selecting Withdraw clears
Payin and Payout, and selecting either of those clears Withdraw.

So if you use withdrawals through the API, plan on **at least two credentials** —
one for your payments traffic and a separate one for withdrawals — and store them
in different places in your secret manager, with different access lists. That is
the point of the separation; putting both in the same environment file gives it
away. Submitting the second credential is the same self-serve flow as the first;
see
[Production Public Keys & API Operation Types](/docs/en/support/production-api-key-and-public-key-faq).

## In what order do you check my API key, scope, signature and IP?

This order, and knowing it tells you which error you will see first when more
than one thing is wrong:

1. **Is `X-API-KEY` present?** If not → `A0001`.
2. **Is it a known, `Active` credential?** If not → `A0001`.
3. **Does that credential's operation type allow this endpoint?** If not →
   `A0004`.
4. **Does `X-SIGNATURE` verify against that credential's public key?** If not →
   `A0002`.
5. **Is the source IP on your allowlist, if you have enabled one?** If not →
   `A0046`.

The practical consequence: **a request that is both signed wrongly and coming
from a non-allowlisted IP reports `A0002`, not `A0046`.** So do not conclude your
allowlist is fine because you have never seen `A0046` — you may simply never have
got past the signature check. Fix errors in the order above.

## What is the difference between "invalid API key" and "invalid signature"?

They point at different halves of the same pair, and each has its own checklist:

| Code | Message | HTTP | What it means | Check |
| - | - | - | - | - |
| `A0001` | Invalid Api secret key | 401 | The `X-API-KEY` is missing, unknown, or not currently `Active` | Header present? Copied from the Integration page of **this** environment? The key's version is `Active`, not `Pending` or `Rejected`? |
| `A0002` | Invalid signature | 401 | The signature did not verify against the public key bound to that API key | Signed the exact bytes you send, with the private key paired to **that** API key's public key, using SHA256withRSA, Base64-encoded? |
| `A0004` | Permission denied | 401 | Valid credential, wrong endpoint for its operation type | The scope table above |
| `A0046` | Access denied: the request IP is not in the whitelist | 401 | Signature was fine; source IP is not allowlisted | Your current egress IP, and the allowlist entries |

After a key rotation, `A0002` almost always means the `X-API-KEY` and the signing
private key no longer belong to each other — switch both together, as one change.

## Is "merchant status abnormal" or "contract does not allow this operation" an authentication problem?

No. Those are account and agreement states, and they arrive **after** your
credential has already been accepted:

| Code | Meaning | Where to look |
| - | - | - |
| `A0006` | Merchant status abnormal | Your account state — contact integration support |
| `A0010` | Operation not allowed in merchant contract | Your agreement does not include this operation or product |
| `A0018` | The payment method in the order does not match your contract | Your contracted payment methods for that country and currency |

The triage shortcut: any `A0001` / `A0002` / `A0004` / `A0046` is a *credential*
problem you can fix yourself. `A0006`, `A0010` and `A0018` are *entitlement*
problems that need either a configuration change on Waffo's side or a different
request. Full list in
[Error codes](/docs/en/developer-docs/tools-and-references/developer-tools/error-codes).

## Do I have to send the API version header?

Yes, on raw HTTP calls: send `X-API-VERSION: 1.0.0`. SDKs manage the header
themselves — do not set or override it there.

One trap worth naming, because it is easy to get wrong from a changelog: the
version numbers you see in the API version history identify **documentation and
contract revisions** and must not be sent in this header. The header value is
`1.0.0`. See [API overview](/docs/en/developer-docs/integration/api/overview).

## How long after I activate or rotate a key can I use it?

Give it **a short while** — it is not instant. Only `Active` credentials verify
signatures, and the lookup used on the request path is refreshed on an interval —
so a key that has just become `Active` may still be rejected for a short window,
and a key that has just stopped being active may still work for the same window.

What follows for a rotation: after the new version becomes `Active`, wait out
that window before cutting traffic over, and keep the old credential in place
until the new one has verified real traffic. Because more than one version can be
`Active` at once, you never need a gap where neither works.

The IP allowlist behaves differently and faster — see below.

## Can I use one API key across several merchant accounts?

No. Each API key belongs to a single merchant account, and a request
authenticated with account A's credential can only act on account A's data.
Querying an order that belongs to account B fails even though the key itself is
perfectly valid.

If you operate several merchant IDs, keep one credential set per account and
always call with the credential of the account that created the object. Building
that routing in from the start is much cheaper than retrofitting it — see
[Multi-brand and sub-merchant setups](/docs/en/support/multi-brand-and-sub-merchant-faq).

## What does the IP allowlist accept, and how quickly does an edit apply?

| Item | Rule |
| - | - |
| Entry formats | **IPv4 addresses and IPv4 CIDR ranges** — for example `203.0.113.7` or `198.51.100.0/24` |
| IPv6 | Not supported |
| Hostnames | Not supported — addresses and ranges only |
| Entries per merchant | Up to **20**; duplicates rejected |
| Per-entry controls | A label, and an individual on/off toggle |
| Master switch | Cannot be turned on with no active entries; deleting or disabling the last active entry turns it off |
| Time to take effect | **Almost immediately** — saving also invalidates the cached lookup |

If 20 entries is not enough, consolidate into CIDR ranges rather than listing
individual hosts — one `/24` replaces up to 254 entries, and a stable range is
also less maintenance than a list that changes every time you scale out.

Enabling it, and the exact Portal fields, are in
[Portal Self-Service Security Configuration](/docs/en/support/portal-security-config-faq).

## My servers are behind proxies, or have dynamic IPs. What actually gets matched?

The address matched is the **first entry in the forwarded-for chain** of the
request as it reaches Waffo — that is, the originating client address, not the
last hop. If that value cannot be parsed as an IPv4 address, the check counts as
"could not decide" rather than as a mismatch.

That distinction drives the outcome, and there are three enforcement policies:

| Policy | Source IP not on the list | Source IP undeterminable |
| - | - | - |
| Observe only | Allowed, and recorded | Allowed, and recorded |
| Enforce (default) | **Blocked** with `A0046` | Allowed, and recorded |
| Strict | **Blocked** with `A0046` | **Blocked** with `A0046` |

The Portal exposes only the master on/off switch; the policy itself is set by
Waffo. So if your egress is dynamic, multi-region, or containerised behind
several layers, ask integration support to put you in **observe only** first,
watch for recorded mismatches, and switch to enforcement once the list is
complete. Turning enforcement on blind is how a correctly-configured integration
takes an outage.

If your egress genuinely cannot be pinned to a set of ranges, the allowlist is
the wrong control for you — it is optional and off by default, and signature
verification is already mandatory on every request.

## How do I find out what my own egress IP is?

Not from Waffo — the address Waffo sees is the address your infrastructure
presents, and the reliable source for it is your own platform: the NAT gateway or
load balancer configuration of your cloud environment, which is also the only
place it is guaranteed to stay stable.

Two practical routes when you cannot get it from configuration:

* **Ask your platform team for the egress ranges** of the environment your
  payment service runs in, and allowlist the ranges rather than the addresses
  you observe. Observed addresses go stale the next time you scale.
* **Run in observe-only first** as described above, so the mismatches themselves
  tell you which addresses are in play before anything is blocked.

## Is my Portal password encrypted in transit? Can Waffo staff see it?

Your password is **encrypted in the browser before it is submitted**, using a
Waffo public key, on top of TLS — so the form never puts a plaintext password on
the wire, not even inside the encrypted channel. The same applies when you set or
change a password.

On the storage side it is kept only as a **one-way hash**, so there is no
recoverable copy to read: nobody at Waffo can retrieve your password, and a
password reset issues you a link rather than telling you the old value.

Password rules, expiry, lockout and sign-in history are on
[Portal Sign-in, Links & Account Status](/docs/en/support/portal-account-access-faq)
and [Merchant Portal Operations](/docs/en/support/portal-operations-faq).

## I think my private key may have leaked. What do I do, in order?

Rotate first, investigate second — and remember that a leaked private key lets
someone sign requests as you, which is exactly as serious as it sounds.

1. **Generate a new key pair** and submit the new public key from the Portal. Your
   existing credential keeps working while it is reviewed, so this step costs you
   no downtime.
2. **When the new version is `Active`, read its API key** from the Integration
   page and switch the `X-API-KEY` and the signing private key together.
3. **Stop using the old credential**, and confirm from your own logs that no
   traffic still carries the old API key.
4. **Turn on the IP allowlist** if you have not already. It is the one control
   that makes a stolen credential unusable from outside your infrastructure, and
   it is the reason to have it configured before you need it.
5. **Review your own handling**: the private key must not be in source control,
   in an image, in a log, or in a browser. The
   [go-live checklist](/docs/en/developer-docs/getting-started/go-live) requires it to
   live in environment configuration or a key management service.
6. **Tell integration support** what happened and when, so activity on the old
   credential can be reviewed from Waffo's side.

Note step 2's failure mode, since it is the most common: reusing the old API key
with the new key pair leaves you verifying against the old public key, so your
new signatures fail while the Portal looks correctly configured.

## Can I capture request and response payloads to debug a signature problem?

Yes, and you should — but capture the **raw bytes**, not a parsed and
re-serialised copy, because a round trip through your JSON library is itself one
of the top causes of a signature that will not verify.

What to log, and what not to:

| Log | Do not log |
| - | - |
| The exact request body bytes you signed | The private key, or any part of it |
| The `X-SIGNATURE` you produced | Full card numbers or other sensitive payment data |
| The `X-API-KEY` in masked form, enough to tell which credential was used | The complete API key |
| The response code and the error code returned | |

The go-live checklist makes the second column a requirement rather than advice:
logs must not record full keys or sensitive payment information. A short-lived,
access-controlled debug capture is the right shape for this — not a permanent
log line.

***


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