Authentication
Workspace-scoped API keys in the X-Api-Key header — scopes, access standing, allow-lists, rotation, and the platform's transport guarantees.
Every request is authenticated with a workspace-scoped API key. This page covers the key's anatomy and storage, the scope catalog, what a key is told when its workspace may not use the API, network allow-lists, rotation, and the guarantees the platform makes in return.
The API key
Keys are issued from the merchant dashboard (Developers → API keys) and have three parts:
- a fixed prefix,
xeg_live_, so keys are recognizable in logs and secret scanners; - a public lookup segment, up to the dot;
- a secret segment after the dot, shown once at issuance.
Everything before the dot (xeg_live_AbCdEfGhIjKl below) is the key's public prefix, as the dashboard and GET /api/v1/me show it. Xegora authenticates a key by a keyed hash of it. For 15 minutes after issuance it also keeps an encrypted copy of the key, solely so that an exact retry of the same issuance (after a dropped connection, say) hands back the same key instead of issuing a second one. After those 15 minutes the copy is purged and only the hash remains: the secret can never be retrieved again — a retried issuance is refused (credential_replay_expired) — so store the key when you create it. A lost key cannot be recovered, only replaced.
Send the full plaintext key on every request:
X-Api-Key: xeg_live_AbCdEfGhIjKl.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
There is no cookie, session, or OAuth handshake on this surface — the header is the whole story.
How a request is refused
Each refusal has its own status and code, so your logs say which part of the setup to look at:
| Status | code | Meaning |
|---|---|---|
| 401 | unauthorized | No key, a malformed key, an unknown, revoked or expired key, or a wrong secret. The problem's message is worded for an API key — this surface has no session to sign in to. |
| 403 | ip_not_allowed | The key and its secret are right, but the request came from an address outside the key's allowed ranges. The key is not used for anything, and the problem's message says to send the request from an allowed address or to issue a key that allows this one. |
| 403 | api_access_inactive | The key is valid, but its workspace may not use the API right now; reason says why (below). |
| 403 | forbidden | The key is valid, but it lacks the scope the endpoint requires. |
The checks run in that order: a workspace that may not use the API is told so before any scope is judged.
Access standing — api_access_inactive
api_access_inactiveA key belongs to its workspace, and the workspace's standing on the API travels with it. When the workspace may not use the API, every refused request is answered 403 with code: "api_access_inactive", a reason, and — when the subscription's paid-through instant is known — paidThroughUtc:
{
"title": "The request could not be completed.",
"status": 403,
"detail": "Use the error code and correlation ID when contacting support.",
"code": "api_access_inactive",
"correlationId": "0198e001a2b37c4d8e9f0a1b2c3d4e5f",
"reason": "subscription_overdue",
"paidThroughUtc": "2026-10-01T00:00:00+00:00",
"message": "The subscription isn't paid, so the API only reads what the workspace already has until the invoice is paid. Pay it in the dashboard."
}The message is written for the reason, not for the code, so each of the four standings below says what to do about that standing — in the response language. Branch on reason, never on the sentence.
reason | Meaning | What still works |
|---|---|---|
workspace_suspended | Xegora suspended the workspace. | Nothing: every request is refused until it is restored. |
setup_fee_unpaid | The plan's setup fee is not paid. | Nothing, until it is paid. |
subscription_overdue | The subscription is not paid up: its paid period ended at paidThroughUtc, or its first period is not paid yet. | Reads (below). |
entitlement_ended | No API entitlement is in force: it ended, expired or was revoked, or none has begun. | Reads (below). |
While the subscription is overdue or the entitlement has ended, the workspace keeps reading what it already has: every GET, POST /orders/{orderId}/fulfillment (the codes of orders you already bought), and POST /withdrawals/quotes. Everything that would change something is refused: POST /quotes, POST /orders, PUT /wallet/deposit-addresses, POST /withdrawals and POST /withdrawals/{withdrawalId}/cancellation. Settle the invoice in the dashboard and the same keys work again — nothing needs to be reissued. GET /api/v1/workspace shows the access status and paid-through instant at any time while reads are allowed.
Scopes — least privilege by design
Each key carries an explicit scope set chosen at issuance. Grant only what the integration needs:
| Scope | Grants | Used by |
|---|---|---|
| (any valid key) | Read the key itself and its workspace — GET /workspace returns its limits block only to a key that also holds wallet.read, and null otherwise | GET /me, GET /workspace |
products.read | Browse the catalog and fetch product images | GET /categories, GET /products, GET /products/filters, GET /products/{productId}, GET /catalog-product-images/{publicReference} |
quotes.create | Price a purchase, and read the quotes the workspace made | POST /quotes, GET /quotes/{quoteId} |
orders.create | Place orders (spends wallet funds) — an order that names its item is priced by the same call — read the workspace's quotes, and reveal what was bought | POST /orders, GET /quotes/{quoteId}, POST /orders/{orderId}/fulfillment |
orders.read | Read orders, their history and how many reveals they have left — never their codes | GET /orders, GET /orders/{orderId}, GET /orders/{orderId}/history, GET /orders/{orderId}/fulfillment/allowance |
orders.reveal | Reveal the delivered codes of the workspace's orders | POST /orders/{orderId}/fulfillment |
wallet.read | Balances, the statement, deposit options, quotes, addresses and history, payout destinations, withdrawal quotes and reads | GET /wallet/*, GET /payout-destinations, POST /withdrawals/quotes, GET /withdrawals, GET /withdrawals/page, GET /withdrawals/{withdrawalId} |
wallet.deposit-address.write | Request the workspace's deposit address for a network and stablecoin | PUT /wallet/deposit-addresses |
wallet.withdraw | Request or cancel a payout of your own funds | POST /withdrawals, POST /withdrawals/{withdrawalId}/cancellation |
Three scopes deserve special attention:
orders.createspends money. A storefront that only displays the catalog should not hold it.- Revealing codes hands over the goods.
orders.readalone never reveals; reveal with a key that holdsorders.reveal, or with the key that buys (orders.create). A support or reporting tool that reads orders should not hold either. wallet.withdrawmoves money out. It is deliberately separate fromwallet.read, so a leaked read-only or ordering key can never trigger a payout. Withdrawals also never name a destination — funds can only go to the payout destination your owners registered and Xegora approved, after a business-day hold, and are cancellable throughout it. See Withdrawals.
A practical split for most integrations:
| Key | Scopes | Lives in |
|---|---|---|
| Storefront key | products.read, quotes.create | Your web/backend catalog layer |
| Purchasing key | orders.create, orders.read | Your order-processing service (it can reveal what it buys) |
| Delivery key | orders.read, orders.reveal | A separate service that hands codes to customers, when you split buying from delivery |
| Treasury key | wallet.read, wallet.deposit-address.write, wallet.withdraw | Your finance tooling only |
GET /api/v1/me lists a key's scopes, expiry and how many address ranges it is restricted to — check it at deploy time rather than learning from a 403.
Network allow-lists and expiry
At issuance you can bind a key to:
- CIDR allow-list — the key is accepted only from these source addresses; from any other address the request is refused
403 ip_not_allowed, and no use of the key is recorded. Strongly recommended for production keys with money-moving scopes. - Expiry — an absolute expiration instant. An expired key authenticates nothing (
401).
Revocation from the dashboard is immediate (401 from then on).
Key hygiene
- Store keys in a secret manager, never in source control, mobile apps, or browser code. The Integration API is a server-to-server surface; calling it from a browser would expose the key.
- Rotate by issuing a new key, deploying it, then revoking the old one — both are valid during the overlap, so rotation needs no downtime.
- Scope one key per system, not one key for everything; a compromise then has a known blast radius.
- A key belongs to the workspace, not to the person who issued it: it keeps working when that person leaves the team or stops managing keys (the workspace's owners are told which keys that person issued). Review and revoke keys you no longer need.
- Watch for
401/403spikes in your logs — they are the earliest sign of a revoked, expired, mis-scoped or address-restricted key, or of a workspace whose access lapsed.
Transport guarantees
- HTTPS only. The API is served exclusively over TLS at
https://myapi.xegora.com. - Workspace isolation. The workspace is derived from the key on every request. No endpoint takes a workspace id — a body that names one is refused as an unknown member — so cross-tenant access is structurally impossible. Another workspace's order, quote, deposit or withdrawal is
404, exactly like one that does not exist. - Strict request bodies. A body member the contract does not have is refused (
400 invalid_request, named inerrors), never silently ignored — a misspeltmaximumTotalcannot order without its ceiling. - Rate limiting. 120 requests per minute per workspace (fixed window), with catalog images on a budget of their own (300 per minute), plus a per-address limit applied before a key is checked. Every admitted response says the budget and what is left of it (
RateLimit-Limit,RateLimit-Remaining); excess requests receive429 rate_limitedwithRetry-After— see Errors & Retries. - No secrets in transit by default. Order bodies and webhooks never contain fulfillment artifacts; codes exist only in the response of an explicit, audited reveal call, always delivered
Cache-Control: no-store. - Auditability. Every mutating call is attributed to the exact API key that made it, and every response — successes and errors alike — returns its correlation id in the
X-Correlation-Idheader, which an error repeats incorrelationId(send your own inX-Correlation-Id).
Updated 18 days ago