Docs

Authentication

Keys, admin keys, environments, scopes and rotation.

Every request under /v1 needs an API key or, from the fxapis console, a signed-in member's session. /healthz and /readyz need neither.

Authorization: Bearer fx_live_7f3a9c21_ZmQ4YmE2MjNhOTg3ZmUxMGJjNGQ...

The bare key without Bearer is also accepted, because people paste both.

The shape of a key

fx_live_7f3a9c21_ZmQ4YmE2MjNhOTg3ZmUxMGJjNGQ...
── ──── ──────── ──────────────────────────────
│  │    │        └ secret — 32 random bytes, base64url
│  │    └ public id — how we find the row; appears in logs
│  └ environment — live or test
└ vendor prefix — makes keys greppable in your codebase

Two parts of that are deliberate and worth knowing about.

The environment is a label, not a sandbox. live and test keys tell your configurations apart — a fx_test_ key in production is easy to spot and grep for — but both reach real brokers. A test key connected to a real-money account places real trades. fxapis has no simulated market: to test, connect demo accounts from your broker (most brokers offer them free) and use those until your integration behaves.

The prefix is fx_. Secret scanners can find it, and so can you: grep -r fx_live_ . before a commit is a habit worth having. If you push one to a public repository, assume it is compromised and rotate it. Keys issued before the fxapis name start ow_; they keep working exactly as issued, and their hint shows ow_, so grep for both.

We cannot recover a key for you

A key is shown once, at creation. We never store the key itself, so nobody — including us — can read it back. Lost keys are replaced, not recovered.

Scopes

Scopes are per key. Issue narrow keys.

ScopeAllows
accounts:readList and read accounts, poll status, read account events, symbols, instruments and sessions, search broker servers; read the workspace, the plans and the plan's usage (GET /v1/billing/usage)
accounts:writeConnect, bring online (one account or many with prepare), take offline, restart, disconnect, delete, change mode, replace the password, move server, re-sync symbols
trading:readRead positions, orders and multi-account orders; reconcile; margin and profit calculations; list TradingView hooks
trading:reduceClose positions, move their stops, cancel pending orders and waves — nothing that opens exposure
trading:executePlace orders and multi-account orders, move pending orders, create, edit and rotate TradingView hooks, and everything trading:reduce allows
history:readRead closed trades and account history. Also reads the event log (GET /v1/events).
billing:readRead the subscription, invoices, usage charges, the agreement, the payment standing and daily usage (GET /v1/billing/subscription, /billing/invoices, /billing/invoices/{id}, /billing/usage-charges, /billing/agreement, /billing/standing, /billing/usage/daily). Not offered to connected apps. A key made before 21 October 2026 that holds accounts:read keeps reading these with it
webhooks:writeCreate, change, test and delete webhook endpoints, and read their delivery logs — see Webhooks
keys:manageCreate, rename, re-scope and revoke this workspace's keys. Not grantable to a key
members:manageInvite, re-role and remove members. Only ever held by a signed-in owner or admin
billing:manageChoose, pay for, change and cancel the plan. Only ever held by the signed-in owner
adminEverything, including key management. Not grantable to a key

A key holding admin passes every scope check, which is why no key can be given it. Keys are created by a signed-in owner or admin — in the console, or with /v1/api-keys from that session — never by another key.

Scope is not ownership. Holding accounts:write lets you write accounts your tenant owns — another tenant's account is not forbidden, it is not found. Returning 403 there would confirm that a guessed id exists.

Limits

Scopes decide what a key may do; limits decide how much it may open. Set them when you create a key, or later — in the console under API keys → Limits, or with limits on POST /v1/api-keys and PATCH /v1/api-keys/{id}:

{
  "limits": {
    "maxVolume": "1.00",
    "symbols": ["EURUSD", "XAUUSD"],
    "accountIds": ["…"],
    "maxOpeningOrdersPerDay": 100
  }
}
LimitMeans
maxVolumeLots per opening order. A multi-account order is checked per account.
symbolsThe broker symbols or instruments it may open, without regard to case. XAUUSD allows gold named by instrument. Orders sent with a broker symbol, including TradingView hook alerts, are matched on that broker symbol, so list the broker's name for it (e.g. GOLD) too if the broker doesn't call it XAUUSD.
accountIdsThe accounts it may open on.
maxOpeningOrdersPerDayOpening orders since 00:00 UTC. Each account in a multi-account order counts as one, including one still waiting to send in an order that has not gone out yet; orders that never left (failed) do not count. The allowance is exact, even for orders sent at the same moment. An order counts on the UTC day it was made: a multi-account order made at 23:50 UTC and sent at 00:10 counts toward the day it was made, once.

Leave a field out for no limit; "limits": null removes them all. Limits apply to every opening order the key places: through the API, a multi-account order (checked when it is made and again just before it is sent), a TradingView hook the key created, or an AI agent using the key over MCP. A refused order answers 403 KEY_LIMIT_EXCEEDED, with details naming the limit, what was asked and what is allowed. Closing, cancelling and moving stops are never limited, and a member working in the console is not a key, so is not limited by any. (An app connected by signing in, over MCP, is held more tightly: everything it changes, closing and stops included, is limited to the accounts picked when it was connected. See MCP server.) A key with limits can change, rotate or re-enable only the TradingView hooks it created. If a key learned a hook's URL before it was given limits — for example, it rotated a teammate's hook — that URL still trades as the hook's maker; rotate the hook to retire the URL.

Good uses: a prop-firm evaluator's key capped at the account's maximum lot size; an AI agent's key that can trade two symbols on one demo account.

Managing keys

From the console (an owner or admin), or with /v1/api-keys and an admin key. Ordinary keys cannot manage keys:

  • Create returns the key in key, once. It is not stored, so it cannot be shown again; the response carries Cache-Control: no-store.
  • List and read show a hint (fx_live_7f3a9c21_…), the scopes, and when the key was last used, to the minute — never the key.
  • Rename and re-scope keep the key itself, so nothing using it redeploys. New scopes apply from its next request.
  • Revoke is refused from the next request, is safe to repeat, and cannot be undone. What the key set up stops with it: a multi-account order it made that has not been sent is abandoned, and a TradingView hook it created stops entirely — exit (close) alerts included. So revoke a key only after moving its hooks, or close their positions another way.

admin, keys:manage, members:manage and billing:manage cannot be put on a key this way, not even by an admin key. A key that could mint keys could mint itself a stronger one. A workspace holds at most 50 unrevoked keys; an expired key counts until it is revoked.

Admin keys

An admin key lets your own tooling — a provisioning script, Terraform, a secrets rotator — manage this workspace's API keys without a person in the console. It is a separate kind of key, with its own prefix:

fx_admin_4c1e9f0a_ZmQ4YmE2MjNhOTg3ZmUxMGJjNGQ...
  • It manages keys and does nothing else. It can list, create, read, rename, re-scope, change the limits of and revoke the workspace's ordinary keys (/v1/api-keys). Every other /v1 route answers 403 ADMIN_KEY_NOT_ALLOWED: trading, accounts, webhooks, events, members, billing, and the workspace itself. Routes outside /v1, including /mcp, answer 401 UNAUTHENTICATED. An ordinary key, whatever its scopes, gets 403 ADMIN_KEY_REQUIRED on /v1/api-keys.
  • It cannot touch admin keys. It does not see them, and the keys it makes hold only the scopes any key may. A key it made names it in createdByAdminKey.
  • Only the owner makes or revokes one. This happens in the console under API keys, after confirming it's you. There is no API for it. At most five can be active, and the owner is emailed whenever one is made or revoked.
  • You can limit where it is used from. Give it IP addresses or ranges (IPv4 or IPv6, such as 203.0.113.7 or 2001:db8::/32). From anywhere else it answers 403 ADMIN_KEY_IP_NOT_ALLOWED.
  • You can give it an expiry. After that day it answers 401 ADMIN_KEY_EXPIRED. The address limits and the expiry are both set when the key is made, and cannot be changed later.
  • Every request it makes is recorded in fxapis's audit trail, with the route, the answer and the address. fxapis staff can see that trail; there is no audit-log view for customers yet. Refused requests are recorded too, but a refusal that repeats is recorded at most once a minute per key and reason.
  • Revoking an admin key does not revoke the keys it made. Revoke those too if they should stop.
  • Step-up does not apply. No admin key is asked to confirm. A wrong, unknown or revoked admin key gets the same 401 UNAUTHENTICATED as any other key.

In the SDKs, key management lives under admin:

  • TypeScript, from 0.2.0: new Fxapis("fx_admin_…").admin.keys.list().
  • Python, from 0.1.6: Fxapis(api_key="fx_admin_…").admin.keys.list().

Rotation

Keys are independent, so rotation needs no downtime:

  1. Create the new key.
  2. Deploy it.
  3. Confirm traffic on the new key — its lastUsedAt in GET /v1/api-keys (updated at most once a minute).
  4. Revoke the old one.

Revocation takes effect on the next request. There is no cache to wait for.

Failures

Every authentication failure returns the same body:

{
  "error": { "code": "UNAUTHENTICATED", "message": "A valid API key is required." },
  "requestId": "3f8a..."
}

Malformed, unknown, wrong secret, revoked, expired — one response for all of them. Telling you which would let someone with a list of candidate keys sort the real ones from the fake. Our logs record the distinction; the response does not.

A key that authenticates but lacks the scope gets MISSING_SCOPE (403) instead, which does name the missing scope — by then you have proven you hold a valid key, and a useful message costs nothing.

Member sessions

The console does not use an API key, and never puts one in the browser. A member signs in with an email and password at /auth, and the session cookie that comes back is accepted under /v1 in place of a key.

It resolves to the same thing a key does: a tenant and a set of scopes. So a member is held by exactly the same checks, and another tenant's account is 404 to them just as it is to a key. The scopes come from their role in the tenant rather than from a list on a key:

RoleScopes
ownerthe read scopes, accounts:write, trading:reduce, webhooks:write, keys:manage, members:manage, billing:manage
adminthe read scopes, accounts:write, trading:reduce, webhooks:write, keys:manage, members:manage
memberaccounts:read, trading:read, history:read, billing:read

The console cannot open a trade directly. It can close positions, move stops and cancel what has not happened yet; opening exposure is done with a key, by your own code — or by a TradingView hook, which an owner or admin can create, and which opens trades from alerts.

No role carries admin. That scope passes every check and is not grantable through the public API; a browser session is the last place to put one.

A few things differ from a key, deliberately:

  • A key always wins. A request with an Authorization header is judged as a key alone. A bad key does not fall back to a cookie sent beside it, and a narrow key does not borrow the scopes of an owner's cookie.
  • Cross-site requests are refused. A browser attaches a cookie on its own, which nobody does with a key. A state-changing request with a session whose Origin is not the console gets 403 UNTRUSTED_ORIGIN.
  • Signing out takes effect on the next request. There is no cached session to wait out, for the same reason revoking a key has none.
  • A session lasts seven days from sign-in, and ends after a day unused. It is never extended; signing in again starts a new one. Under Security in the console, members see every browser they are signed in on, sign any of them out, and change their password, which always signs every other session out and is emailed to them.
  • Sensitive changes ask you to confirm it's you. Making a key that can move money or accounts (trading:execute, trading:reduce or accounts:write), widening a key to one of those scopes or changing limits on a key that already has them, inviting, changing a role, removing someone and changing the plan or payment need a confirmation from the last two hours: the authenticator code with two-factor on, otherwise the password. A read-only key, narrowing a key's scopes and renaming never ask. Signing in counts. Without one the API answers 403 STEP_UP_REQUIRED, with details[0].method (totp or password), and the console asks and carries on. A key is never asked. Staff keep a ten-minute window in the staff console.
  • Repeated wrong passwords are slowed down. After three for an account, or from one network, the sign-in page asks you to pass a quick check; after ten in fifteen minutes the account is paused for fifteen minutes and its owner is emailed. Resetting the password lifts the pause.
  • Two-factor belongs to the session. Turning it on or off, or making new backup codes, signs every other session out. Turning it off needs the current code as well as the password. There is no "trust this device".
  • Console traffic is not metered as API calls, and is rate limited in buckets of its own, so looking at your usage neither costs usage nor spends the budget your integration needed.

Anyone may sign up with an email and a password, and becomes the owner of a new workspace. It starts a 14-day trial — demo accounts only, no card — once per person; a sign-up that does not get a trial starts on the free plan while one is offered, otherwise with no plan until they choose one. Sign-ups can be closed for a while, when the API answers SIGNUPS_CLOSED (an invitation still works). GitHub and Google sign-in are not offered yet.

Members

A workspace can have several people in it, all seeing the same accounts, keys and usage:

OwnerAdminMember
Read accounts, orders, usage✓✓✓
Close, move stops, cancel✓✓
Manage keys✓✓
Make and revoke admin keys✓
Manage TradingView hooks✓✓
Choose and pay for the plan✓
Inviteadmins and membersmembers
Change roles✓
Removeadmins and membersmembers

Nobody changes or removes the owner, and nobody acts on themselves. People are managed by people. Every /v1/members route refuses API keys and admin keys, and none of them is in the SDKs or the Postman collection. The API reference marks them "Console only".

Invitations are emailed to the address, and the link is returned too, to send another way. POST /v1/members/invitations/{id}/resend emails it again (at most once a minute). Inviting an address that already has an open invitation returns that one — the same link — rather than a second. A link works once, expires after seven days, and makes its holder a member of that workspace with that role; nothing they send can change either. The link is a signature over the invitation's id, so it is not stored anywhere it could be stolen from.

Removing a member signs them out everywhere on their next request, deletes their password, and revokes every API key they made in the console: anything still using those keys gets 401 from its next request. Each revocation is in the audit log, and the owner is emailed the list. Keys made by anyone else, made with an admin key, or made before keys recorded their maker (createdBy is null) are not touched. What they did stays attributed to them. Suspension by fxapis staff revokes the same keys, and lifting it does not bring them back. If an owner is suspended or moved to another role, the admin keys that owner made are revoked too; the keys those admin keys made keep working.

Demoting an admin to member revokes the same keys, in the same change: members cannot make keys, so none an admin made may outlive the role. Each revocation is audited and the owner is emailed the list, as for a removal; PATCH /v1/members/{id} returns how many were revoked (keysRevoked), and GET /v1/members shows how many each person's demotion or removal would revoke (keysCreated). Making a member an admin revokes nothing. A role changed by fxapis staff follows the same rule.

One person belongs to one workspace: an address that already has an account cannot be invited to another.

On this page