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 worth internalising before you read any 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, a webhook subscription, 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"
}

What the key cannot do

Administering the merchant is not part of this API. Members, API keys, IP whitelists and webhook subscriptions are created and changed by a person in the cabinet. The API can read webhook subscriptions — including the secret you verify signatures with — but there is no route to create, edit or delete one.

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/merchant/withdrawals
POST /api/v1/aml/check-address

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.

IP whitelisting

A key can additionally be pinned to a set of source addresses (Settings → IP whitelist).

  • An empty whitelist means no filtering — the key works from anywhere.
  • Add a single entry and every other source address is refused with 403 CallerIpNotAllowedError, even though the key itself is perfectly valid.

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 InvalidApiKeyError Header missing, malformed, unknown or revoked. All four are deliberately indistinguishable.
403 CallerIpNotAllowedError Valid key, presented from an address its whitelist does not cover.
403 MerchantIsBlockedError The merchant has been blocked.
403 PermissionDeniedError The resource you named belongs to another merchant.

A blocked merchant cannot read either

MerchantIsBlockedError 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 PermissionDeniedError 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.