Authentication
InfraIO Pay has two API surfaces with different auth models. Pick the one that matches who’s calling:
| Surface | Path prefix | Audience | Auth |
|---|---|---|---|
| Merchant B2B | /b2b/v1/* | Your server | HMAC-SHA256 request signing |
| Dashboard | Used by the merchant dashboard | Browser sessions for the merchant dashboard | Bearer JWT |
This page covers the B2B surface, the one you call from your server with an API key pair. The dashboard surface is used by the InfraIO Pay merchant dashboard and isn’t a public integration surface.
Always send the full path including the /b2b prefix, and sign that
same path (see below).
Endpoints
| Environment | Base URL |
|---|---|
| Test | https://api-dev.infraio.xyz |
| Live | https://api.infraio.xyz |
Same URL pattern — environment is controlled by the key prefix
(pk_test_… vs pk_live_…), not the URL.
Key pair
You get two values from the merchant dashboard (Developers → API keys → + Add key):
- Publishable key (
pk_test_…orpk_live_…) — identifies your account. Sent asX-Client-ID. Safe to embed in your browser bundle (the SDK already does). - Secret key (
sk_test_…orsk_live_…) — the HMAC signing key. Server-only. Treat it like a database password.
If a secret key ever lands in a browser bundle, git repo, log line, or shared chat — revoke it immediately from the dashboard. Revocation is instant, with no overlap window. Issue a new key and redeploy.
Signing a request
Every call to /b2b/v1/* carries three headers:
X-Client-ID: pk_live_70d0becbd7a53047b0dfe88a960d10ad
X-Timestamp: 1715990400
X-Signature: 9a8b7c6d… (hex HMAC-SHA256)The signature is computed over a canonical string:
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODYMETHOD— uppercase HTTP verb (POST,GET, …).PATH— request path including the/b2bprefix, without the host and without the query string (e.g./b2b/v1/checkout-sessions/quick). The prefix must be present. Query parameters are not signed — for aGET …?cursor=…&limit=20, sign only the path, not the?…part.TIMESTAMP— unix seconds, as decimal string (e.g."1715990400"), matchingX-Timestampexactly.BODY— raw request body bytes. Empty string forGET/DELETE.
Sign with HMAC-SHA256 keyed by the secret key, output hex:
Node / TS
import { createHmac } from "node:crypto";
function sign({ method, path, body, secret }: {
method: string; path: string; body: string; secret: string;
}) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const input = [method.toUpperCase(), path, timestamp, body].join("\n");
const signature = createHmac("sha256", secret).update(input).digest("hex");
return { timestamp, signature };
}Why HMAC, not Bearer?
A bare Bearer-token API ships your only secret over the wire on every request. Anyone who captures one TLS-terminated proxy log gets the keys to your account. HMAC signing means the secret never travels — only its derived signature, which is single-use (bound to that exact request + that exact minute).
The trade-off: you compute a signature for every call. There is no server SDK yet, but the helper above is about 15 lines per language.
Timestamp tolerance
The tolerance is ±5 minutes (300 seconds). A request outside that
window is rejected with 401 invalid_signature. Two
implications:
- Sync your server clock with NTP. A long-running cron with a drifted clock will fail intermittently.
- Don’t pre-compute and queue signatures. If a request sits in a retry queue for >5 min, its signature expires.
Key scopes
Secret keys carry one or more of these scope bundles:
| Scope | Intended use |
|---|---|
read | List/read orders, sessions, refunds |
write_order | Create checkout sessions, orders |
write_refund | Issue refunds, mint refund-request tokens |
webhook_manage | Create/update/delete webhook endpoints |
The dashboard issues a “full access” key by default (all four scopes). You can mint a restricted-scope key from Developers → API keys → + Add key and tick only the scopes the integration needs.
Scopes are not enforced yet. Scopes are recorded on the key and
shown in the dashboard, but any valid sk_… key can call any
/b2b/v1/* endpoint for your merchant. Don’t rely on scopes as a
security boundary. Rotate or revoke keys to restrict access.
Failed verification
If the signature, X-Client-ID, or timestamp is invalid, the request is
rejected with 401 INVALID_SIGNATURE before it reaches the API. Only
/b2b/v1/* requests are signed this way. Webhooks use a separate scheme
(see below).
What’s next
- Errors — response shape on 4xx/5xx.
- Security → API keys — rotation, revocation, what to do if a secret leaks.
- Webhooks → Signature verification
— uses a different HMAC scheme (header
X-Signature: sha256=…, signsX-Timestamp + "." + raw_body, plus an optionalX-Signature-Prevduring the 24-hour rotation grace window). Don’t mix the schemes up — they share the hash algorithm but the signed bytes and the secret family (whsec_…vssk_…) are different.