Authentication
Two kinds of keys for two audiences. Both are sent in a header, both carry scopes, both can be limited to an IP list and an expiry date.
#Sending a key
| Partner API | Trading API | |
|---|---|---|
| Header | X-Api-Key: <key> | X-API-Key: qk_… |
| Alternative | Authorization: Bearer <key> | Authorization: Bearer qk_… |
| Who creates it | CRM owner / admin, Settings › API & Webhooks › API keys | The client, in the trader under API access (Pro mode) |
| Belongs to | The company (all brands the key is allowed to see) | One client and their trading accounts |
| Format | Opaque string, shown once | qk_ + 48 characters, shown once |
| Missing / wrong key | 401 invalid_api_key | 401 api_key_required / invalid_api_key / api_key_expired / api_key_revoked |
#Partner API keys
A Partner key represents your backend. When it is created the owner picks the scopes it carries, optionally an IP allow-list (calls from other addresses answer 403 ip_not_allowed) and an expiry. Keys can be revoked at any time; the delivery log and every client history line show which key did what.
curl "https://api.your-brand.com/api/external/v1/me" \
-H "X-Api-Key: $OCTOLIT_PARTNER_KEY"const res = await fetch("https://api.your-brand.com/api/external/v1/me", {
method: "GET",
headers: {
"X-Api-Key": process.env.OCTOLIT_PARTNER_KEY,
},
});
if (!res.ok) throw new Error(`GET failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);import os, requests
r = requests.request(
"GET",
"https://api.your-brand.com/api/external/v1/me",
headers={"X-Api-Key": os.environ["OCTOLIT_PARTNER_KEY"]},
timeout=10,
)
r.raise_for_status()
print(r.json())<?php
$ch = curl_init("https://api.your-brand.com/api/external/v1/me");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: " . getenv("OCTOLIT_PARTNER_KEY"),
],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Get, "https://api.your-brand.com/api/external/v1/me");
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("OCTOLIT_PARTNER_KEY"));
using var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://api.your-brand.com/api/external/v1/me"))
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());var body io.Reader
req, err := http.NewRequest("GET", "https://api.your-brand.com/api/external/v1/me", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-Api-Key", os.Getenv("OCTOLIT_PARTNER_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))var req = URLRequest(url: URL(string: "https://api.your-brand.com/api/external/v1/me")!)
req.httpMethod = "GET"
req.setValue(ProcessInfo.processInfo.environment["OCTOLIT_PARTNER_KEY"], forHTTPHeaderField: "X-Api-Key")
let (data, response) = try await URLSession.shared.data(for: req)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self))val client = OkHttpClient()
val req = Request.Builder()
.url("https://api.your-brand.com/api/external/v1/me")
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.get()
.build()
client.newCall(req).execute().use { res ->
println("${res.code} ${res.body?.string()}")
} A key sees only the brands its company allows. Per brand the Partner API switch must be on, otherwise calls answer 403 brand_disabled.
#Partner scopes
A call that needs a scope the key does not carry answers 403 insufficient_scope. The scope each operation needs is shown as a chip on the endpoint.
clients:readRead the clients of the company, page by change time (GET /clients?updated_since=…).clients:read_emailE-mail addresses in clear instead of masked.clients:read_phonePhone numbers in clear instead of masked.clients:read_ftdFirst-deposit date on clients.clients:read_commentsClient comments.clients:read_custom_fieldsCustom fields of the client.users:writePUT /users/{external_id} - create or update a client by your id.sso:issuePOST /users/{external_id}/sso - one-time login links; list and end sessions.accounts:readAccounts, balance and positions of a user; quotes, bars and trending for your middleware.accounts:writeOpen a trading account for a user.trading:controlPause and resume trading for a user.mt:linkRecord which MT4 / MT5 login belongs to a user.trading:actPlace, change and close orders FOR a user through the platform's own order path; every action lands in the client's history as done by your key.balance:writePOST /users/{external_id}/transactions - book a deposit, withdrawal or credit. OFF by default: only the CRM owner (or an admin with the "Edit transactions" right) can put it on a key, and the brand's "Money posting" switch must be on.#Trading API keys
A Trading key represents one client. The broker switches the Trading API on for the brand; the client then sees API access in the trader and creates up to 5 keys. Each key is read-only or trading, may carry an IP allow-list and an expiry, and can be revoked by the client, the broker or the system (revoking also closes its WebSocket and FIX sessions).
curl "https://api.your-brand.com/api/open/v1/account" \
-H "X-API-Key: $OCTOLIT_API_KEY"const res = await fetch("https://api.your-brand.com/api/open/v1/account", {
method: "GET",
headers: {
"X-API-Key": process.env.OCTOLIT_API_KEY,
},
});
if (!res.ok) throw new Error(`GET failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);import os, requests
r = requests.request(
"GET",
"https://api.your-brand.com/api/open/v1/account",
headers={"X-API-Key": os.environ["OCTOLIT_API_KEY"]},
timeout=10,
)
r.raise_for_status()
print(r.json())<?php
$ch = curl_init("https://api.your-brand.com/api/open/v1/account");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: " . getenv("OCTOLIT_API_KEY"),
],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Get, "https://api.your-brand.com/api/open/v1/account");
req.Headers.Add("X-API-Key", Environment.GetEnvironmentVariable("OCTOLIT_API_KEY"));
using var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync());var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://api.your-brand.com/api/open/v1/account"))
.header("X-API-Key", System.getenv("OCTOLIT_API_KEY"))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());var body io.Reader
req, err := http.NewRequest("GET", "https://api.your-brand.com/api/open/v1/account", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-API-Key", os.Getenv("OCTOLIT_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))var req = URLRequest(url: URL(string: "https://api.your-brand.com/api/open/v1/account")!)
req.httpMethod = "GET"
req.setValue(ProcessInfo.processInfo.environment["OCTOLIT_API_KEY"], forHTTPHeaderField: "X-API-Key")
let (data, response) = try await URLSession.shared.data(for: req)
print((response as! HTTPURLResponse).statusCode, String(decoding: data, as: UTF8.self))val client = OkHttpClient()
val req = Request.Builder()
.url("https://api.your-brand.com/api/open/v1/account")
.header("X-API-Key", System.getenv("OCTOLIT_API_KEY"))
.get()
.build()
client.newCall(req).execute().use { res ->
println("${res.code} ${res.body?.string()}")
} When the Trading API is not available the answer is 403 open_api_disabled with details.reason: brand_off (the broker has not enabled it), pro_locked (the client is not in Pro mode), server_off or not_ready.
#Trading scopes
readAccount, symbols, quotes, bars, trending, positions, orders, history, stream tickets. Every key has it.tradePlace, change, close and cancel orders. Chosen when the key is created; the broker can stop API trading at any time.fixLog on over FIX 4.4 with the same key (QUOTE and TRADE sessions).A read-only key that tries to place an order answers 403 trade_scope_required. The broker can stop API trading for everyone at once (403 api_trading_stopped); reads keep working.
#SSO tokens, webhook secrets, FIX
| Surface | Credential | Where |
|---|---|---|
| Inbound SSO | A JWT you sign (RS256 public key / JWKS URL, or an HS256 shared secret) | Settings › API & Webhooks › Broker login (SSO), per brand |
| Outbound SSO | A one-time login link issued with a Partner key (sso:issue) | POST /users/{external_id}/sso |
| Webhooks | Per-endpoint secret whsec_… used to sign every delivery (HMAC-SHA256) | Settings › API & Webhooks › Webhooks; rotate with 24 h overlap |
| FIX 4.4 | The Trading API key in tag 554 at logon; SenderCompID K<key id> | Key needs the fix scope |
#Key hygiene
- Store keys in a secrets manager or environment variable; the examples read
OCTOLIT_PARTNER_KEY/OCTOLIT_API_KEY. - Give each system its own key with the smallest scope set. A key that posts money (
balance:write) should always have an IP allow-list. - Set an expiry and rotate: create the new key, switch your service, revoke the old one. Both work during the overlap.
- On
401stop retrying - the key is gone. On403read the code: it is a switch or a scope, not a transient fault. - Watch the
client.loginevent withmethod: "sso"or"link"to see sign-ins your keys caused.
# Rotation without downtime
export OCTOLIT_PARTNER_KEY="<new key>" # 1. deploy the new key
curl -s https://api.your-brand.com/api/external/v1/me -H "X-Api-Key: $OCTOLIT_PARTNER_KEY" # 2. confirm
# 3. revoke the old key in Settings > API & Webhooks