# Authentication

URL: https://docs.fxapis.com/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 [#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 [#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]

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](/guides/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 [#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}`:

```json
{
  "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](/guides/mcp#safety-model).)
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 [#managing-keys]

From the console (an owner or admin), or with `/v1/api-keys` and an
[admin key](#admin-keys). 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 [#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 [#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 [#failures]

Every authentication failure returns the same body:

```json
{
  "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 [#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](/guides/tradingview), 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 [#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.
