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

# Portal Self-Service Security Configuration

> How to manage API security settings in the Merchant Portal: submitting and rotating public keys with challenge-string verification, configuring refund and chargeback notification URLs, and restricting API access with an IP allowlist.

This page covers the security settings you can manage yourself on
**Merchant Portal → Integration**. For request signing mechanics, see the
[Authentication & Signature FAQ](/docs/en/support/auth-signature-faq).

## Can I add or rotate my public key myself in the Merchant Portal?

Yes. Production public keys are submitted online through the Integration page.
Each submission registers a **new** key record with its own API key — you do
not edit an existing key in place. The submission goes through Waffo review:

| Status | Meaning |
| - | - |
| Pending | Submitted, awaiting Waffo review |
| Active | Approved — used to verify your request signatures |
| Rejected | Not approved — you can correct and resubmit |

You receive an email when the key is activated or rejected (rejection emails
include the reason). To rotate keys, submit the new public key, wait for it to
become Active, then switch your integration to the new API key.

## Why does submitting a public key require signing a challenge string?

To prove you actually hold the private key paired with the public key you are
uploading. The Portal shows a fixed challenge string in the form
`WAFFO_VERIFY_{mid}`; sign it locally with your private key and submit the
signature together with the public key. Waffo verifies the signature with the
submitted public key before accepting it.

| Item | Requirement |
| - | - |
| Key type | RSA 2048-bit, X.509 format, Base64 encoded |
| Signature algorithm | SHA256withRSA (RSASSA-PKCS1-v1\_5) |
| Signature encoding | Base64 |

The same public key cannot be submitted twice for the same merchant.

## How many API keys and public keys can I have?

Each API key maps to exactly **one** public key. A merchant can hold multiple
API keys, scoped by API operation type:

| Operation type | Scope |
| - | - |
| `PAYIN_ONLY` | Payment, refund, and query APIs |
| `PAY_OUT_ONLY` | Payout and transfer APIs |
| `PAYIN_AND_PAYOUT` | All payin and payout APIs |
| `WITHDRAW` | Withdrawal APIs |

A per-operation-type limit applies to how many public keys you can hold; it is
generous enough to keep old and new keys active in parallel during a rotation,
and the Portal tells you when you reach it. The count includes submissions that
were rejected, so clean up abandoned attempts if you approach the limit.

## Where do I configure the refund and chargeback notification URLs?

Also on **Merchant Portal → Integration**. Unlike public keys, notification
URL changes need **no approval** — once the URL passes validation, it takes
effect immediately. Validation rules: the URL must start with `http://` or
`https://`, conform to RFC 3986 (no spaces or non-ASCII characters), and be at
most 512 characters. Clearing the field and saving deletes the configuration.

## Can I set different notification URLs per API key?

No. Each MID has one global Refund Notify URL and one global Chargeback
Notify URL — they are not stored per API operation type. The Chargeback
Notify URL applies to card acquiring only. Withdrawal notifications do not
use these settings: the withdrawal notify URL is passed per order in the API
request.

## How do I restrict API access to my servers' IP addresses?

Enable the **IP allowlist** in the Portal — an optional safeguard that is off
by default. Once enabled, a request whose signature is valid but whose source
IP is not on your allowlist is rejected with error `A0046` ("Access denied:
the request IP is not in the whitelist", HTTP 401). Configuration changes take
effect shortly after you save — saving also invalidates the cached lookup, so in
practice it is usually immediate. Merchants that never enable it see no behavior
change.

The allowlist is evaluated **after** signature verification, so a request with
a bad signature fails on the signature error (`A0002`) rather than `A0046`,
even when its IP is also off the list.

## What formats and limits does the IP allowlist support?

* Entries accept **IPv4 addresses** and **CIDR ranges** (for example
  `203.0.113.7` or `198.51.100.0/24`). IPv6 is not currently supported.
* Up to **20 entries** per merchant; duplicates are rejected.
* Each entry can carry a label and be toggled on or off individually.
* The allowlist cannot be switched on with no active entries, and deleting or
  disabling the last active entry automatically turns the master switch off.

## What happens if Waffo cannot determine my request's source IP?

By default, a request whose source IP cannot be determined is **allowed** and
logged — only an IP that is determined and not on your allowlist is blocked.

Waffo supports stricter and looser enforcement policies than this default, but
the Portal exposes only the master on/off switch; the policy itself is set on
Waffo's side. Contact integration support if you want to run in
observe-only mode first, or if you need requests blocked even when the source
IP cannot be determined.

## How does the two-step verification challenge work at sign-in?

Two-step verification is a **sign-in** control rather than an Integration
setting — the account-side questions (who can switch it on, what to do when
codes never arrive) are in
[Merchant Portal Operations](/docs/en/support/portal-operations-faq). Mechanically it
is checked *during* the login call, not after it:

1. You submit the sign-in form as usual. If the account has a confirmed two-step
   method, no session is issued: the sign-in comes back as a
   **challenge required** response, and a code is already on its way to the
   account's login email.
2. The Portal opens the *Verification* dialog with a short-lived resend token
   attached to that sign-in attempt.
3. You enter the code and the Portal repeats the **same sign-in**, this time
   with the code. Only then is a session issued.

Only the account's **default** method is challenged. A wrong or expired code
fails the sign-in itself, which is why the dialog asks you to re-enter the code
and not the password.

## Why does Resend stop working if I leave the sign-in page open?

At that point you have no session, so Resend cannot be authorised by one. It is
authorised by the resend token handed out with the challenge. That token belongs
to a single sign-in attempt and expires after a while; reloading the page or
starting a new attempt discards it. If Resend fails, sign in again — that issues
a fresh challenge and a fresh token.

## What does each two-step verification control actually do?

All of these are Portal actions performed with your **Portal session** — they
are not part of the API-key-signed merchant API and cannot be scripted against
with your integration keys.

| Control | What it does |
| - | - |
| Switching it **on** (`Settings → Preference`) | Opens a binding for the email method and sends the first code |
| *Resend* inside the binding dialog | Sends another code for the same binding |
| *Verify* inside the binding dialog | Confirms the code and completes the binding |
| Switching it **off** | Unbinds the method; asks for your account password |
| *Resend* inside the sign-in dialog | Sends another code for the current sign-in attempt, using the resend token |
| The `2-Step Verification` column in the operator list (Admin) | Enables or disables the email method for another operator |

Three consequences are worth knowing:

* **A binding only counts once the code is verified.** Closing the dialog after
  the code was sent leaves the attempt unconfirmed and two-step verification
  off; switching the toggle on again starts a fresh round.
* **The first method you confirm becomes the default** — and the default is the
  one the sign-in challenge uses.
* **Switching off is authorised by your password, not by a code**, travelling
  encrypted the same way the sign-in form's password does. A wrong password
  returns the standard wrong-password error and the method stays bound.


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