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:

StatuscodeMeaning
401unauthorizedNo 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.
403ip_not_allowedThe 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.
403api_access_inactiveThe key is valid, but its workspace may not use the API right now; reason says why (below).
403forbiddenThe 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

A 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.

reasonMeaningWhat still works
workspace_suspendedXegora suspended the workspace.Nothing: every request is refused until it is restored.
setup_fee_unpaidThe plan's setup fee is not paid.Nothing, until it is paid.
subscription_overdueThe subscription is not paid up: its paid period ended at paidThroughUtc, or its first period is not paid yet.Reads (below).
entitlement_endedNo 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:

ScopeGrantsUsed 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 otherwiseGET /me, GET /workspace
products.readBrowse the catalog and fetch product imagesGET /categories, GET /products, GET /products/filters, GET /products/{productId}, GET /catalog-product-images/{publicReference}
quotes.createPrice a purchase, and read the quotes the workspace madePOST /quotes, GET /quotes/{quoteId}
orders.createPlace 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 boughtPOST /orders, GET /quotes/{quoteId}, POST /orders/{orderId}/fulfillment
orders.readRead orders, their history and how many reveals they have left — never their codesGET /orders, GET /orders/{orderId}, GET /orders/{orderId}/history, GET /orders/{orderId}/fulfillment/allowance
orders.revealReveal the delivered codes of the workspace's ordersPOST /orders/{orderId}/fulfillment
wallet.readBalances, the statement, deposit options, quotes, addresses and history, payout destinations, withdrawal quotes and readsGET /wallet/*, GET /payout-destinations, POST /withdrawals/quotes, GET /withdrawals, GET /withdrawals/page, GET /withdrawals/{withdrawalId}
wallet.deposit-address.writeRequest the workspace's deposit address for a network and stablecoinPUT /wallet/deposit-addresses
wallet.withdrawRequest or cancel a payout of your own fundsPOST /withdrawals, POST /withdrawals/{withdrawalId}/cancellation

Three scopes deserve special attention:

  • orders.create spends money. A storefront that only displays the catalog should not hold it.
  • Revealing codes hands over the goods. orders.read alone never reveals; reveal with a key that holds orders.reveal, or with the key that buys (orders.create). A support or reporting tool that reads orders should not hold either.
  • wallet.withdraw moves money out. It is deliberately separate from wallet.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:

KeyScopesLives in
Storefront keyproducts.read, quotes.createYour web/backend catalog layer
Purchasing keyorders.create, orders.readYour order-processing service (it can reveal what it buys)
Delivery keyorders.read, orders.revealA separate service that hands codes to customers, when you split buying from delivery
Treasury keywallet.read, wallet.deposit-address.write, wallet.withdrawYour 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/403 spikes 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 in errors), never silently ignored — a misspelt maximumTotal cannot 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 receive 429 rate_limited with Retry-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-Id header, which an error repeats in correlationId (send your own in X-Correlation-Id).

Did this page help you?