Money posting
You run the payments. Octolit runs the trading account. Money posting is the one call that connects them: your cashier confirms a payment, you book it on the account, the client trades. Every posting is idempotent, audited and reflected in the balance at once.
#The principle
POST …/users/{external_id}/transactionsbalance:write - off by default201 booked · 200 replayed#Before you start
- The brand switch Money posting is on (
403 brand_disabledotherwise). - Your key carries
balance:write. Only the CRM owner or an admin with the "Edit transactions" right can put that scope on a key - treat the key like a payment credential: IP allow-list, short expiry, never on the web tier. - You know the user's
account_idvalues (GET …/accounts). Withoutaccount_idthe first account is used. - You have a stable reference per payment on your side (PSP id, invoice number). It becomes the idempotency key.
#Post a deposit
Your payment provider confirmed a card payment of 250 EUR. Book it as approved; the balance is updated in the same call.
curl -X POST "https://api.your-brand.com/api/external/v1/users/cust_10042/transactions" \
-H "X-Api-Key: $OCTOLIT_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "deposit",
"amount": "250.00",
"currency": "EUR",
"account_id": 88213,
"status": "approved",
"method": "card",
"reference": "psp_9f31c2",
"idempotency_key": "dep-psp_9f31c2"
}'const res = await fetch("https://api.your-brand.com/api/external/v1/users/cust_10042/transactions", {
method: "POST",
headers: {
"X-Api-Key": process.env.OCTOLIT_PARTNER_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"kind": "deposit",
"amount": "250.00",
"currency": "EUR",
"account_id": 88213,
"status": "approved",
"method": "card",
"reference": "psp_9f31c2",
"idempotency_key": "dep-psp_9f31c2"
}),
});
if (!res.ok) throw new Error(`POST failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);import os, requests
r = requests.request(
"POST",
"https://api.your-brand.com/api/external/v1/users/cust_10042/transactions",
headers={"X-Api-Key": os.environ["OCTOLIT_PARTNER_KEY"]},
json={
"kind": "deposit",
"amount": "250.00",
"currency": "EUR",
"account_id": 88213,
"status": "approved",
"method": "card",
"reference": "psp_9f31c2",
"idempotency_key": "dep-psp_9f31c2",
},
timeout=10,
)
r.raise_for_status()
print(r.json())<?php
$ch = curl_init("https://api.your-brand.com/api/external/v1/users/cust_10042/transactions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: " . getenv("OCTOLIT_PARTNER_KEY"),
"Content-Type: " . "application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"kind" => "deposit",
"amount" => "250.00",
"currency" => "EUR",
"account_id" => 88213,
"status" => "approved",
"method" => "card",
"reference" => "psp_9f31c2",
"idempotency_key" => "dep-psp_9f31c2",
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.your-brand.com/api/external/v1/users/cust_10042/transactions");
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("OCTOLIT_PARTNER_KEY"));
req.Content = new StringContent("""
{
"kind": "deposit",
"amount": "250.00",
"currency": "EUR",
"account_id": 88213,
"status": "approved",
"method": "card",
"reference": "psp_9f31c2",
"idempotency_key": "dep-psp_9f31c2"
}
""", Encoding.UTF8, "application/json");
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/users/cust_10042/transactions"))
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"kind": "deposit",
"amount": "250.00",
"currency": "EUR",
"account_id": 88213,
"status": "approved",
"method": "card",
"reference": "psp_9f31c2",
"idempotency_key": "dep-psp_9f31c2"
}
"""))
.build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());body := strings.NewReader(`{
"kind": "deposit",
"amount": "250.00",
"currency": "EUR",
"account_id": 88213,
"status": "approved",
"method": "card",
"reference": "psp_9f31c2",
"idempotency_key": "dep-psp_9f31c2"
}`)
req, err := http.NewRequest("POST", "https://api.your-brand.com/api/external/v1/users/cust_10042/transactions", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-Api-Key", os.Getenv("OCTOLIT_PARTNER_KEY"))
req.Header.Set("Content-Type", "application/json")
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/users/cust_10042/transactions")!)
req.httpMethod = "POST"
req.setValue(ProcessInfo.processInfo.environment["OCTOLIT_PARTNER_KEY"], forHTTPHeaderField: "X-Api-Key")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = """
{
"kind": "deposit",
"amount": "250.00",
"currency": "EUR",
"account_id": 88213,
"status": "approved",
"method": "card",
"reference": "psp_9f31c2",
"idempotency_key": "dep-psp_9f31c2"
}
""".data(using: .utf8)
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/users/cust_10042/transactions")
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.method("POST", """{
"kind": "deposit",
"amount": "250.00",
"currency": "EUR",
"account_id": 88213,
"status": "approved",
"method": "card",
"reference": "psp_9f31c2",
"idempotency_key": "dep-psp_9f31c2"
}""".toRequestBody("application/json".toMediaType()))
.build()
client.newCall(req).execute().use { res ->
println("${res.code} ${res.body?.string()}")
} {
"transaction": {
"id": 551203,
"kind": "deposit",
"status": "approved",
"amount": "250.00",
"currency": "EUR",
"account_id": 88213,
"method": "card",
"reference": "psp_9f31c2",
"note": null,
"created_at": "2026-11-11T09:40:12Z"
},
"balance": {
"currency": "EUR",
"balance": "250.00",
"equity": "250.00",
"margin": "0",
"free_margin": "250.00",
"margin_level": null,
"open_pl": "0",
"credit": "0",
"as_of": "2026-11-11T09:40:12Z"
},
"idempotency_key": "dep-psp_9f31c2",
"replayed": false
}amountis a string to keep the exact decimals (up to 10). Numbers are accepted too.currencydefaults to the account's currency. Sending it is a cheap guard against booking EUR on a USD account (422).methodis free text shown in the CRM (defaultpartner_api);referenceis your id and is sent back in events;noteis for your staff.transaction.idis the CRM transaction id. It is the same id thedeposit.statusevent carries - store it next to your payment.- Every posting leaves an audit row and a line in the client's history naming the key: "Partner API deposit via key '…'".
#Kinds and status
kind | Effect | Event |
|---|---|---|
deposit | Cash in. Raises the balance; sets the first-deposit date on the client the first time; triggers the deposit e-mails and postbacks the CRM is configured for. | deposit.status |
withdrawal | Cash out. Refused over the free margin (409 insufficient_funds). Usually status: pending so the CRM back office approves it. | withdrawal.status |
credit | Credit in: a credit line (bonus) the client can trade with but not withdraw. Shown separately as credit. | - |
credit_out | Credit removed. Refused over the current credit (409 insufficient_funds). | - |
status is approved (default) or pending:
- approved counts at once. Use it when your side already settled the payment.
- pending is booked but waits for approval in the Octolit CRM (back office). When staff approve or decline,
deposit.status/withdrawal.statusarrive withprevious_status. Use it for withdrawals, or for deposits your compliance team must look at first.
The money is booked by the same transaction service a back-office "Add transaction" uses: balance update, first-deposit date, client e-mails and postbacks all behave the same, and for an MT account the balance operation is sent to the MT server before the answer.
#Idempotency
Payments are retried by everyone - your PSP webhook, your job runner, your own code after a timeout. The idempotency key makes every retry safe:
Derive the key from the payment
Use your PSP's id or your own payment row id, with a short prefix:
dep-psp_9f31c2. Never a timestamp, never a random value per attempt. Send it asidempotency_keyin the body or as theIdempotency-Keyheader - either, not both with different values.Repeat the same call as often as you like
The second call with the same key and the same body returns the stored answer:
200withreplayed: trueand the originaltransaction.id. Nothing is booked twice.Handle the two conflicts
Same key, different body →
409 idempotency_conflict: a bug on your side, do not "fix" it by changing the key. Same key while the first call is still running →409 in_progress: wait a second and send the identical call again.
{
"error": {
"code": "idempotency_conflict",
"message": "The same Idempotency-Key was used with a different body."
}
}Keys are unique per company, across all your keys and users. A key is never reusable, even after a year.
#Withdrawals
A withdrawal is checked against the account's free margin, not the balance: with open positions the client cannot withdraw margin that is in use. Over the limit the call answers 409 insufficient_funds with details.available and books nothing. The usual pattern is status: pending - the withdrawal waits for approval in the CRM, and your back office approves it after the payout went out.
curl -X POST "https://api.your-brand.com/api/external/v1/users/cust_10042/transactions" \
-H "X-Api-Key: $OCTOLIT_PARTNER_KEY" \
-H "Idempotency-Key: wd-req_20481" \
-H "Content-Type: application/json" \
-d '{
"kind": "withdrawal",
"amount": "100.00",
"account_id": 88213,
"status": "pending",
"method": "bank",
"reference": "req_20481",
"note": "IBAN ending 4471"
}'const res = await fetch("https://api.your-brand.com/api/external/v1/users/cust_10042/transactions", {
method: "POST",
headers: {
"X-Api-Key": process.env.OCTOLIT_PARTNER_KEY,
"Idempotency-Key": "wd-req_20481",
"Content-Type": "application/json",
},
body: JSON.stringify({
"kind": "withdrawal",
"amount": "100.00",
"account_id": 88213,
"status": "pending",
"method": "bank",
"reference": "req_20481",
"note": "IBAN ending 4471"
}),
});
if (!res.ok) throw new Error(`POST failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);import os, requests
r = requests.request(
"POST",
"https://api.your-brand.com/api/external/v1/users/cust_10042/transactions",
headers={"X-Api-Key": os.environ["OCTOLIT_PARTNER_KEY"], "Idempotency-Key": "wd-req_20481"},
json={
"kind": "withdrawal",
"amount": "100.00",
"account_id": 88213,
"status": "pending",
"method": "bank",
"reference": "req_20481",
"note": "IBAN ending 4471",
},
timeout=10,
)
r.raise_for_status()
print(r.json())<?php
$ch = curl_init("https://api.your-brand.com/api/external/v1/users/cust_10042/transactions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: " . getenv("OCTOLIT_PARTNER_KEY"),
"Idempotency-Key: " . "wd-req_20481",
"Content-Type: " . "application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"kind" => "withdrawal",
"amount" => "100.00",
"account_id" => 88213,
"status" => "pending",
"method" => "bank",
"reference" => "req_20481",
"note" => "IBAN ending 4471",
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.your-brand.com/api/external/v1/users/cust_10042/transactions");
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("OCTOLIT_PARTNER_KEY"));
req.Headers.Add("Idempotency-Key", "wd-req_20481");
req.Content = new StringContent("""
{
"kind": "withdrawal",
"amount": "100.00",
"account_id": 88213,
"status": "pending",
"method": "bank",
"reference": "req_20481",
"note": "IBAN ending 4471"
}
""", Encoding.UTF8, "application/json");
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/users/cust_10042/transactions"))
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.header("Idempotency-Key", "wd-req_20481")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"kind": "withdrawal",
"amount": "100.00",
"account_id": 88213,
"status": "pending",
"method": "bank",
"reference": "req_20481",
"note": "IBAN ending 4471"
}
"""))
.build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());body := strings.NewReader(`{
"kind": "withdrawal",
"amount": "100.00",
"account_id": 88213,
"status": "pending",
"method": "bank",
"reference": "req_20481",
"note": "IBAN ending 4471"
}`)
req, err := http.NewRequest("POST", "https://api.your-brand.com/api/external/v1/users/cust_10042/transactions", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-Api-Key", os.Getenv("OCTOLIT_PARTNER_KEY"))
req.Header.Set("Idempotency-Key", "wd-req_20481")
req.Header.Set("Content-Type", "application/json")
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/users/cust_10042/transactions")!)
req.httpMethod = "POST"
req.setValue(ProcessInfo.processInfo.environment["OCTOLIT_PARTNER_KEY"], forHTTPHeaderField: "X-Api-Key")
req.setValue("wd-req_20481", forHTTPHeaderField: "Idempotency-Key")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = """
{
"kind": "withdrawal",
"amount": "100.00",
"account_id": 88213,
"status": "pending",
"method": "bank",
"reference": "req_20481",
"note": "IBAN ending 4471"
}
""".data(using: .utf8)
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/users/cust_10042/transactions")
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.header("Idempotency-Key", "wd-req_20481")
.method("POST", """{
"kind": "withdrawal",
"amount": "100.00",
"account_id": 88213,
"status": "pending",
"method": "bank",
"reference": "req_20481",
"note": "IBAN ending 4471"
}""".toRequestBody("application/json".toMediaType()))
.build()
client.newCall(req).execute().use { res ->
println("${res.code} ${res.body?.string()}")
} {
"error": {
"code": "insufficient_funds",
"message": "The withdrawal exceeds the free margin.",
"details": {
"available": "221.20"
}
}
}When the payout fails on your side, your staff decline the pending transaction in the CRM; withdrawal.status tells you with the new status and previous_status.
#Credit (bonus)
credit adds to the credit line, credit_out removes from it. Credit is shown separately as credit in every balance answer and is never cash: it cannot be withdrawn, and a credit_out over the current credit answers 409 insufficient_funds. Use a key that encodes the campaign and the client so a bonus is never granted twice.
curl -X POST "https://api.your-brand.com/api/external/v1/users/cust_10042/transactions" \
-H "X-Api-Key: $OCTOLIT_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "credit",
"amount": "50",
"account_id": 88213,
"reference": "welcome-bonus",
"note": "Welcome bonus, 30 days",
"idempotency_key": "bonus-cust_10042-2026-11"
}'const res = await fetch("https://api.your-brand.com/api/external/v1/users/cust_10042/transactions", {
method: "POST",
headers: {
"X-Api-Key": process.env.OCTOLIT_PARTNER_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"kind": "credit",
"amount": "50",
"account_id": 88213,
"reference": "welcome-bonus",
"note": "Welcome bonus, 30 days",
"idempotency_key": "bonus-cust_10042-2026-11"
}),
});
if (!res.ok) throw new Error(`POST failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);import os, requests
r = requests.request(
"POST",
"https://api.your-brand.com/api/external/v1/users/cust_10042/transactions",
headers={"X-Api-Key": os.environ["OCTOLIT_PARTNER_KEY"]},
json={
"kind": "credit",
"amount": "50",
"account_id": 88213,
"reference": "welcome-bonus",
"note": "Welcome bonus, 30 days",
"idempotency_key": "bonus-cust_10042-2026-11",
},
timeout=10,
)
r.raise_for_status()
print(r.json())<?php
$ch = curl_init("https://api.your-brand.com/api/external/v1/users/cust_10042/transactions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: " . getenv("OCTOLIT_PARTNER_KEY"),
"Content-Type: " . "application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"kind" => "credit",
"amount" => "50",
"account_id" => 88213,
"reference" => "welcome-bonus",
"note" => "Welcome bonus, 30 days",
"idempotency_key" => "bonus-cust_10042-2026-11",
]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);using var http = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Post, "https://api.your-brand.com/api/external/v1/users/cust_10042/transactions");
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("OCTOLIT_PARTNER_KEY"));
req.Content = new StringContent("""
{
"kind": "credit",
"amount": "50",
"account_id": 88213,
"reference": "welcome-bonus",
"note": "Welcome bonus, 30 days",
"idempotency_key": "bonus-cust_10042-2026-11"
}
""", Encoding.UTF8, "application/json");
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/users/cust_10042/transactions"))
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"kind": "credit",
"amount": "50",
"account_id": 88213,
"reference": "welcome-bonus",
"note": "Welcome bonus, 30 days",
"idempotency_key": "bonus-cust_10042-2026-11"
}
"""))
.build();
var res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());body := strings.NewReader(`{
"kind": "credit",
"amount": "50",
"account_id": 88213,
"reference": "welcome-bonus",
"note": "Welcome bonus, 30 days",
"idempotency_key": "bonus-cust_10042-2026-11"
}`)
req, err := http.NewRequest("POST", "https://api.your-brand.com/api/external/v1/users/cust_10042/transactions", body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-Api-Key", os.Getenv("OCTOLIT_PARTNER_KEY"))
req.Header.Set("Content-Type", "application/json")
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/users/cust_10042/transactions")!)
req.httpMethod = "POST"
req.setValue(ProcessInfo.processInfo.environment["OCTOLIT_PARTNER_KEY"], forHTTPHeaderField: "X-Api-Key")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = """
{
"kind": "credit",
"amount": "50",
"account_id": 88213,
"reference": "welcome-bonus",
"note": "Welcome bonus, 30 days",
"idempotency_key": "bonus-cust_10042-2026-11"
}
""".data(using: .utf8)
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/users/cust_10042/transactions")
.header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY"))
.method("POST", """{
"kind": "credit",
"amount": "50",
"account_id": 88213,
"reference": "welcome-bonus",
"note": "Welcome bonus, 30 days",
"idempotency_key": "bonus-cust_10042-2026-11"
}""".toRequestBody("application/json".toMediaType()))
.build()
client.newCall(req).execute().use { res ->
println("${res.code} ${res.body?.string()}")
} #Reconcile
Two sources, use both:
-
The balance endpoint - the truth right now
GET …/balancereturns the user's figures across all accounts plus one entry per account.balanceis cash,creditthe credit line,equityincludes open profit and loss,as_ofsays how fresh the numbers are. For MT accounts these are the last figures the connector reported.GET /api/external/v1/users/cust_10042/balancecurl "https://api.your-brand.com/api/external/v1/users/cust_10042/balance" \ -H "X-Api-Key: $OCTOLIT_PARTNER_KEY"const res = await fetch("https://api.your-brand.com/api/external/v1/users/cust_10042/balance", { 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/users/cust_10042/balance", 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/users/cust_10042/balance"); 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/users/cust_10042/balance"); 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/users/cust_10042/balance")) .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/users/cust_10042/balance", 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/users/cust_10042/balance")!) 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/users/cust_10042/balance") .header("X-Api-Key", System.getenv("OCTOLIT_PARTNER_KEY")) .get() .build() client.newCall(req).execute().use { res -> println("${res.code} ${res.body?.string()}") }200 OK{ "balance": { "currency": "EUR", "balance": "250.00", "equity": "262.40", "margin": "41.20", "free_margin": "221.20", "margin_level": "636.9", "open_pl": "12.40", "credit": "50", "as_of": "2026-11-11T10:02:00Z", "accounts": [ { "id": 88213, "name": "Main EUR", "currency": "EUR", "demo": false, "type": { "id": 4, "name": "Standard", "provider": "internal" }, "balance": "250.00", "equity": "262.40", "margin": "41.20", "free_margin": "221.20", "margin_level": "636.9", "open_pl": "12.40", "closed_pl": "0", "credit": "50", "mt_login": null, "created_at": "2026-11-11T09:31:00Z" } ] } } -
Webhooks - every change as it happens
deposit.statusandwithdrawal.statusfire for every transaction whose status changes - your postings and the ones your staff enter in the CRM alike. Match ondata.transaction.id(your postings) or onreference, and ondata.client.external_idfor the client. Unknown ids are deposits you did not post: book them on your side. -
A nightly comparison
Sum your approved postings per account and compare with
balanceminus trading results. A difference means a posting you lost (checkreplayedanswers you ignored) or a back-office entry you did not consume. The CRM's transaction list shows themethodandreferenceof every row for exactly this.
#Demo vs live
- Demo accounts take postings too - that is how you top up a practice account from your own UI. The same endpoint, the same key rules; the account's
demo: truetells them apart in your books. - Never mix: derive the key from the account as well when the same payment reference can exist in both worlds (
demo-topup-88214-2026-11-11). - Send
account_idon every posting. Without it the user's first account is used - which may be the live one.
#Errors
brand_disabledThe brand's "Money posting" switch is off.insufficient_scopeThe key has no balance:write.-No client with this external_id.idempotency_conflictSame key, different body. Pick a new key for a new posting.in_progressThe first call with this key is still being booked. Wait a moment and repeat the same call.insufficient_fundsWithdrawal over free margin or credit_out over credit; details.available tells how much there is.invalid_requestA field is wrong; details.errors names it (amount not > 0, unknown currency, account not the user's).money_rejectedThe CRM's transaction service refused the posting (for example trading is paused for the user). Nothing was booked.Rule of thumb: on 409 in_progress, 429 and 5xx retry the identical call with backoff. On every other error stop and alert; nothing was booked.
#Go-live checklist
- The key with
balance:writelives only on the server that talks to your PSP; IP allow-list set. - Every posting carries an idempotency key derived from the payment, and your retry sends the identical body.
transaction.idis stored next to the payment;replayed: trueis treated as success.- Withdrawals are posted
pendingand your back office approves them in the CRM after the payout. deposit.status/withdrawal.statusare consumed and unknown transactions are booked on your side.- A nightly comparison against
GET …/balanceruns and alerts on differences. - Tested on a demo account first, then with one live posting of a small amount.
Reference: post a transaction, balance, deposit.status, error codes.