curl -X POST https://api.liddie.io/api/v1/whitelist/ips \
-H "Cookie: <dashboard session cookies>" \
-H "Content-Type: application/json" \
-d '{"ip":"203.0.113.10","label":"Office"}'
// Dashboard-session auth: send the session JWT as a Bearer token
const res = await fetch('https://api.liddie.io/api/v1/whitelist/ips', {
method: 'POST',
headers: {
Authorization: `Bearer ${dashboardJwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
ip: '203.0.113.10',
label: 'Office',
// ...plus one fresh MFA factor (email code, OTP, or passkey assertion)
}),
})
const body = await res.json()
{
"ok": true,
"data.id": "<string>"
}Whitelist
Add an IP to the withdrawal whitelist
Add an IP address that can request Liddie withdrawals from your account, gated by a fresh MFA factor (email code, TOTP, or passkey) from the dashboard user.
POST
/
api
/
v1
/
whitelist
/
ips
curl -X POST https://api.liddie.io/api/v1/whitelist/ips \
-H "Cookie: <dashboard session cookies>" \
-H "Content-Type: application/json" \
-d '{"ip":"203.0.113.10","label":"Office"}'
// Dashboard-session auth: send the session JWT as a Bearer token
const res = await fetch('https://api.liddie.io/api/v1/whitelist/ips', {
method: 'POST',
headers: {
Authorization: `Bearer ${dashboardJwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
ip: '203.0.113.10',
label: 'Office',
// ...plus one fresh MFA factor (email code, OTP, or passkey assertion)
}),
})
const body = await res.json()
{
"ok": true,
"data.id": "<string>"
}Adds an IP address to the list allowed to REQUEST withdrawals. It does not affect signing in — the IP whitelist gates egress only.
Adding your first IP turns egress into an allowlist. While your IP list is empty nothing is
restricted. From the moment it holds one entry,
POST /withdrawals, POST /mass-payouts and
POST /admin/withdrawals accept requests only from a listed address; anything else is refused with
403 IP_NOT_WHITELISTED. Add the address you actually call from before you rely on it, and add every
one you use — an office and a CI runner are two entries, not one.The check runs before the MFA factor is verified, so a blocked attempt does not consume your emailed
code: whitelist the address and retry with the same one, for the rest of its 15 minutes.You can always add an IP from an unlisted address. The whitelist-management endpoints are
deliberately not IP-gated — otherwise a changed ISP address would lock you out permanently, with no way
to add the one you are calling from. The trade-off is explicit: the IP list is defence in depth and an
alarm, not an unbypassable boundary. Someone holding both your session and a working second factor
could add their own address — but not quietly. Every IP added, and every blocked attempt, emails the
account owner with their anti-phishing code and is written to the audit log.
Cookie-authenticated writes also need an
Origin header. Any non-GET request that carries the liddie_access session cookie is checked against the allowed dashboard origins; a bare cURL that sends only the cookie is rejected with 403 {"ok":false,"error":{"code":"CSRF_ORIGIN_MISMATCH","message":"Forbidden"}} before the handler runs. The snippet below is shown for shape — from a browser the dashboard sends the origin for you; from a script, prefer an API key where the endpoint accepts one.curl -X POST https://api.liddie.io/api/v1/whitelist/ips \
-H "Cookie: <dashboard session cookies>" \
-H "Content-Type: application/json" \
-d '{"ip":"203.0.113.10","label":"Office"}'
// Dashboard-session auth: send the session JWT as a Bearer token
const res = await fetch('https://api.liddie.io/api/v1/whitelist/ips', {
method: 'POST',
headers: {
Authorization: `Bearer ${dashboardJwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
ip: '203.0.113.10',
label: 'Office',
// ...plus one fresh MFA factor (email code, OTP, or passkey assertion)
}),
})
const body = await res.json()
Authorization
Dashboard-only (JWT session). Roles:merchant_admin / merchant_member / super_admin, with team permission whitelist:manage. The body must additionally carry a fresh MFA factor (passkey / TOTP / emailed code); request an email code via Send whitelist email code. Rate limit: 10/min.
Calling with an API key fails with 401 {"error":"Invalid or expired token"} — the key is not a JWT.
Parameters
string
required
The IP address to whitelist, e.g.
203.0.113.10. 3–64 characters.string
A human-readable name for the entry, e.g.
"Office". Max 64 characters.MFA fields (dashboard sessions)
MFA fields (dashboard sessions)
string
The single-use emailed code.
string
A 6-digit code from your authenticator app — an alternative to
emailCode. Backup codes are not accepted here: the field is validated against ^[0-9]{6}$ and backup codes are 16 hexadecimal characters. They work only at login (POST /2fa/validate). If you have lost your authenticator, request an emailed code instead.object
WebAuthn assertion, used together with
challengeKey.string
Accompanies
passkeyResponse.Request a fresh single-use code via Send whitelist email code right before this call, then include it in the body as the email-code verification factor — alternatively, an OTP or passkey assertion satisfies the MFA requirement.
Response
201 Created. The body echoes only the new entry’s id — nothing else about the entry is returned.
201 Created
{ "ok": true, "data": { "id": "665f1a2b3c4d5e6f70819200" } }
boolean
true on success.string
The whitelist entry id. Pass it to Remove whitelist entry to undo this.
Errors
| Status | Body | Why |
|---|---|---|
| 400 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Verification required: provide email code, OTP, or passkey"}} | No (or invalid) MFA factor in the body. |
| 401 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Invalid email code"}} | Wrong, already-used or invalid email code, OTP or passkey assertion. Same code, different status — do not treat this 401 as an expired session. |
| 403 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"TOTP is not enabled on this account"}} | A totpCode was sent by a user with no authenticator enrolled. |
| 429 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Too many incorrect codes. Request a new one."}} | Five wrong email codes — the challenge is voided; request a fresh code. |
| 429 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Too many failed attempts. Try again later."}} | TOTP lockout after repeated wrong codes. |
| 401 | {"error":"Invalid or expired token"} | Called with an API key (or an expired session) — this endpoint is dashboard-JWT only. |
| 400 | {"ok":false,"error":{"code":"WHITELIST_ADD_FAILED","message":"Invalid IP address (IPv4 or IPv6 expected)"}} | Only a literal address is accepted — no CIDR ranges, no hostnames. |
| 409 | {"ok":false,"error":{"code":"WHITELIST_ADD_FAILED","message":"This IP is already whitelisted"}} | Already on the list. |
| 409 | {"ok":false,"error":{"code":"WHITELIST_ADD_FAILED","message":"Maximum of 50 whitelisted IPs reached"}} | The per-merchant IP cap (50). |
Every
WHITELIST_ADD_FAILED fires AFTER the MFA factor is verified, so an emailed code is already spent when you see one. Request a fresh code before retrying.See also
- Send whitelist email code: get the single-use email code this endpoint accepts as an MFA factor.
- Get whitelist: list the wallet and IP entries currently on the whitelist.
- Remove whitelist entry: remove a wallet or IP entry (also MFA-gated).