Skip to main content

Authentication

Most endpoints are scoped to a merchant owner address and authenticated by a short-lived session key that the merchant registered ahead of time. The session key is just an EVM private key the client controls — its public half is bound to the owner via POST /auth/register-pubkey.

Every authenticated request carries two headers:

HeaderValue
x-session-nonceStrictly-increasing integer (millisecond timestamp works)
x-session-signatureEIP-191 signature over sess:{nonce}:{sha256(body)}

The server recovers the EVM address from the signature, looks up the matching session key in its registry, and uses the bound owner as the request's authenticated identity. There's no X-Owner-Address header on the wire — the owner is implied by the signing key.

{sha256(body)} is the lowercase hex digest of the request body bytes; for empty-body calls (e.g. POST /poll/events with no payload) hash the empty string.

Optional headers

HeaderDefaultEffect
x-encryption: noneencrypt-when-registeredOpt out of encrypted responses. Useful for backend integrations that don't carry the owner's encryption key.
x-session-pubkeyinferred from signatureDisambiguates between multiple registered keys for the same owner. Only needed when more than one key is active.

Signing snippets

import { Wallet, sha256, toUtf8Bytes } from 'ethers';

const SESSION_PRIVATE_KEY = '0x...';
const body = JSON.stringify({ /* … */ });
const nonce = Date.now();
const hash = sha256(toUtf8Bytes(body)).slice(2);
const sig = await new Wallet(SESSION_PRIVATE_KEY)
.signMessage(`sess:${nonce}:${hash}`);

await fetch('https://api.feemaker.io/invoice', {
method: 'POST',
headers: {
'x-session-nonce': String(nonce),
'x-session-signature': sig,
'x-encryption': 'none',
},
body,
});

Replay protection

Each registered session key has a per-key nonce counter. The server only accepts a request when its x-session-nonce is strictly greater than the last value it saw for that key. Use a millisecond timestamp or a monotonic counter — never reuse a nonce.

Soft auth vs strict auth

When the server is in strict-auth mode every owner-scoped endpoint requires session headers. In soft-auth mode they're optional, but if you send them they must validate — an expired or unregistered key is rejected, never silently treated as "unauthenticated".

Public, payer-side reads (e.g. fetching an invoice by guid for a checkout page) don't require headers in either mode.