Skip to content

Authentication

Every request under /api/v1 carries your merchant API key in the x-api-key header. There is no sign-in, no token exchange and no session to keep alive. The two endpoints outside that prefix, /docs and /health, need no key.

curl https://api.pay.nullswap.com/api/v1/invoices?limit=20 \
  -H "x-api-key: sk_live_2f8b1c0d4e5a69738f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e"

The key is sk_live_ followed by 64 hexadecimal characters. It is not a bearer token: an Authorization: Bearer ... header is ignored, and the API will answer 401 as if no key had been sent at all.

One key names one merchant

Your key identifies exactly one merchant. That has a consequence which applies to every other page:

No route anywhere in this API takes a merchant id

Not in a path, not in a query string, not in a request body. Every list is narrowed to your merchant before anything is read, and every write is attributed to it. If you are looking for a merchantId field to send, there isn't one.

merchantId does appear in responses — on an invoice or a static wallet — but purely as an echo, and it will always be your own.

Read the merchant your key belongs to with:

curl https://api.pay.nullswap.com/api/v1/merchant \
  -H "x-api-key: sk_live_..."
{
  "id": "9f1d2c3b-4a5e-4f60-8b71-2c3d4e5f6a7b",
  "name": "Acme Store",
  "status": "ACTIVE",
  "createdAt": "2026-01-01T12:00:00.000Z",
  "updatedAt": "2026-01-01T12:00:00.000Z",
  "feePercentage": "150"
}

feePercentage is the merchant fee we charge on your payments, in basis points ("150" is 1.50%). We set it; there is no route to change it. It is withheld from each sweep onto your balance — see the merchant fee.

What the key cannot do

Administering the merchant is not part of this API. Members, API keys, IP whitelists, the withdrawal address whitelist, auto-conversion rules and webhook subscriptions are created and changed by a person in the cabinet. The API has no webhook routes at all: it cannot create, edit, delete or read a subscription, its delivery history or the secret you verify signatures with, which you read in the cabinet.

Concretely, the merchant API has six write routes and nothing else:

Method Path
POST /api/v1/invoices
POST /api/v1/invoices/{invoiceId}/payments/{paymentId}/resolve
POST /api/v1/invoices/{invoiceId}/payments/{paymentId}/refund
POST /api/v1/static-wallets
POST /api/v1/wallets/{walletId}/sweep
POST /api/v1/merchant/withdrawals

There are no PUT, PATCH or DELETE routes.

Keep the key server-side

The key is a bearer credential in the plain sense: whoever holds it can create invoices, move your money out and read your entire payment history.

Never ship the key to a browser or a mobile app

Call the API from your backend only. If a key leaks, rotate it from the cabinet — a rotated key stops working on the next request, because keys are not cached.

A merchant has one key at a time, under Settings → API key in the cabinet. A new merchant has none: its owner issues the first one with Issue a key. Only the owner can Regenerate it, which revokes the old key at once. Issuing and regenerating ask for a two-factor code when the owner has two-factor authentication turned on.

The key is shown once

The cabinet shows the full key only right after you issue or regenerate it. Copy it into your backend's secret storage then: we keep only its last four characters, so afterwards every member sees it masked (••••1d0e) and nobody, us included, can show it again. If you lose it, regenerate it and update your integrations.

IP whitelisting

API access can additionally be restricted to a set of source addresses, on the API access by IP card under Settings → API key. The list belongs to the merchant, not to the key, so it survives a key rotation. Only the owner can change it. Settings → Whitelists is a different list: the withdrawal addresses.

  • An empty whitelist means no filtering — the key works from anywhere.
  • Add a single entry and every other source address is refused with 403 CALLER_IP_NOT_ALLOWED, even though the key itself is perfectly valid.
  • An entry is an IPv4 address in which any octet may be * (203.0.113.*), an exact IPv6 address, or a lone * for any address. CIDR ranges are not accepted, and IPv6 takes no wildcards.

A blocked merchant is refused with MERCHANT_BLOCKED before its whitelist is consulted.

The address we compare against is the TCP peer address of the connection. X-Forwarded-For is not trusted and cannot be used to change it, so if your traffic leaves through a NAT or an egress proxy, whitelist that address rather than your application server's.

Errors

Status message Cause
401 INVALID_API_KEY Header missing, malformed, unknown or revoked. All four are deliberately indistinguishable.
403 CALLER_IP_NOT_ALLOWED Valid key, presented from an address its whitelist does not cover.
403 MERCHANT_BLOCKED The merchant has been blocked.
403 PERMISSION_DENIED The resource you named belongs to another merchant.

A blocked merchant cannot read either

MERCHANT_BLOCKED is raised during authentication, before the route runs, so it applies to every authenticated endpoint — reads included. If you are treating 403 as "this write was rejected", widen it.

Note also the distinction between 403 PERMISSION_DENIED and 404: asking for an invoice id that belongs to a different merchant is a 403, not a 404. Asking for an id that exists nowhere is a 404.