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 codebaseTwo 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.
| Scope | Allows |
|---|---|
accounts:read | List 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:write | Connect, bring online (one account or many with prepare), take offline, restart, disconnect, delete, change mode, replace the password, move server, re-sync symbols |
trading:read | Read positions, orders and multi-account orders; reconcile; margin and profit calculations; list TradingView hooks |
trading:reduce | Close positions, move their stops, cancel pending orders and waves — nothing that opens exposure |
trading:execute | Place orders and multi-account orders, move pending orders, create, edit and rotate TradingView hooks, and everything trading:reduce allows |
history:read | Read closed trades and account history. Also reads the event log (GET /v1/events). |
billing:read | Read 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:write | Create, change, test and delete webhook endpoints, and read their delivery logs — see Webhooks |
keys:manage | Create, rename, re-scope and revoke this workspace's keys. Not grantable to a key |
members:manage | Invite, re-role and remove members. Only ever held by a signed-in owner or admin |
billing:manage | Choose, pay for, change and cancel the plan. Only ever held by the signed-in owner |
admin | Everything, 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
}
}| Limit | Means |
|---|---|
maxVolume | Lots per opening order. A multi-account order is checked per account. |
symbols | The 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. |
accountIds | The accounts it may open on. |
maxOpeningOrdersPerDay | Opening 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 carriesCache-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/v1route answers403 ADMIN_KEY_NOT_ALLOWED: trading, accounts, webhooks, events, members, billing, and the workspace itself. Routes outside/v1, including/mcp, answer401 UNAUTHENTICATED. An ordinary key, whatever its scopes, gets403 ADMIN_KEY_REQUIREDon/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.7or2001:db8::/32). From anywhere else it answers403 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 UNAUTHENTICATEDas 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:
- Create the new key.
- Deploy it.
- Confirm traffic on the new key — its
lastUsedAtinGET /v1/api-keys(updated at most once a minute). - 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:
| Role | Scopes |
|---|---|
owner | the read scopes, accounts:write, trading:reduce, webhooks:write, keys:manage, members:manage, billing:manage |
admin | the read scopes, accounts:write, trading:reduce, webhooks:write, keys:manage, members:manage |
member | accounts: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
Authorizationheader 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
Originis not the console gets403 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:reduceoraccounts: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 answers403 STEP_UP_REQUIRED, withdetails[0].method(totporpassword), 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:
| Owner | Admin | Member | |
|---|---|---|---|
| Read accounts, orders, usage | ✓ | ✓ | ✓ |
| Close, move stops, cancel | ✓ | ✓ | |
| Manage keys | ✓ | ✓ | |
| Make and revoke admin keys | ✓ | ||
| Manage TradingView hooks | ✓ | ✓ | |
| Choose and pay for the plan | ✓ | ||
| Invite | admins and members | members | |
| Change roles | ✓ | ||
| Remove | admins and members | members |
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.
Migrating from MetaApi to fxapis
A concept-by-concept guide for moving an MT5 integration from MetaApi to fxapis: provisioning and deploy versus connect and modes, RPC and streaming versus REST and polling, a table of equivalent calls, and what has no equivalent yet — streaming, MT4 and CopyFactory features.
Accounts
Connecting MT5 accounts, keeping them online, and what each state means.