Quickstart
From no credentials to a signed, authenticated request. Everything below runs against your real account. There is no sandbox, so the first call is a read.
Open Key Management
Sign in to Vanta and go to Settings → Key Management. You will need a prop account already, because the API trades an account you have and cannot create one.
Create a key for one account
Pick the account the key should trade, give the key a name you will recognise later (the strategy or machine it runs on), and create it. The key is bound to that account permanently: it cannot read or trade any other account you own, and there is no way to re-point it later.
You can hold up to 3 active keys per account. Revoke one to free a slot.
Copy the secret, which is shown once
Creating a key returns a key id (
vk_…) and a secret (vks_…). The secret is displayed a single time and is stored encrypted; it is never shown again and support cannot recover it. If you lose it, revoke the key and create another.Treat the secret like a password
Anyone holding it can place orders on that account. Keep it out of source control, out of shared notebooks, and out of your browser. Read it from an environment variable or a secret manager, and revoke it immediately if it leaks.Put the credentials in your environment
.env / shellexport VANTA_KEY_ID="vk_your_key_id" export VANTA_SECRET="vks_your_secret" # Only needed for placing orders. This is the account the key is bound to. export VANTA_ACCOUNT_ID="your-prop-account-uuid"The account id is the
accountIdreturned by the account endpoint in step 5, and is also in the dashboard URL when that account is selected.Make your first signed request
This reads your account snapshot: balance, equity, challenge progress and drawdown. It changes nothing, so it is the safe call to get your signing right on.
# Replace with your key and secret (secret is shown only when you create the key). # The API key is bound to a single prop account, so no accountId is required. export VANTA_KEY_ID="YOUR_KEY_ID" export VANTA_SECRET="YOUR_SECRET" export BASE_URL="https://app.vantatrading.io" PATH_REQ="/api/v1/trading/account" TS=$(($(date +%s) * 1000)) NONCE=$(openssl rand -hex 16) # GET has an empty body, so this is the sha256 of the empty string BODY_HASH=$(printf '' | openssl dgst -sha256 -binary | xxd -p -c 256) CANONICAL=$(printf 'v1\nGET\n%s\n%s\n%s\n%s' "$PATH_REQ" "$TS" "$NONCE" "$BODY_HASH") SIG=$(echo -n "$CANONICAL" | openssl dgst -sha256 -hmac "$VANTA_SECRET" -binary | base64 | tr -d '\n') curl -s "$BASE_URL$PATH_REQ" \ -H "X-Vanta-Key-Id: $VANTA_KEY_ID" \ -H "X-Vanta-Timestamp: $TS" \ -H "X-Vanta-Nonce: $NONCE" \ -H "X-Vanta-Signature: v1=$SIG"# pip install requests import base64 import hashlib import hmac import os import secrets import time import requests # The secret is shown once, when the key is created. Keep it out of source # control. Read it from the environment or a secret manager. KEY_ID = os.environ["VANTA_KEY_ID"] SECRET = os.environ["VANTA_SECRET"] BASE_URL = "https://app.vantatrading.io" def signed_headers(method: str, path: str, body: str = "") -> dict: """Build the four auth headers for one request. `path` must be the request target exactly as it is sent: the pathname plus the query string, if any. Signing a different string than you request is the most common cause of a 401. """ timestamp = str(int(time.time() * 1000)) nonce = secrets.token_hex(16) canonical = "\n".join( [ "v1", method.upper(), path, timestamp, nonce, hashlib.sha256(body.encode("utf-8")).hexdigest(), ] ) signature = base64.b64encode( hmac.new( SECRET.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256 ).digest() ).decode("ascii") return { "X-Vanta-Key-Id": KEY_ID, "X-Vanta-Timestamp": timestamp, "X-Vanta-Nonce": nonce, "X-Vanta-Signature": f"v1={signature}", } path = "/api/v1/trading/account" response = requests.get( BASE_URL + path, headers=signed_headers("GET", path), timeout=30 ) print(response.status_code, response.json())// Node 18 or newer. No dependencies: `fetch` and `node:crypto` are built in. import { createHash, createHmac, randomBytes } from "node:crypto"; // The secret is shown once, when the key is created. Keep it out of source // control. Read it from the environment or a secret manager. const KEY_ID = process.env.VANTA_KEY_ID; const SECRET = process.env.VANTA_SECRET; const BASE_URL = "https://app.vantatrading.io"; /** * Build the four auth headers for one request. `path` must be the request * target exactly as it is sent: pathname plus query string, if any. */ function signedHeaders(method, path, body = "") { const timestamp = String(Date.now()); const nonce = randomBytes(16).toString("hex"); const canonical = [ "v1", method.toUpperCase(), path, timestamp, nonce, createHash("sha256").update(body, "utf8").digest("hex"), ].join("\n"); const signature = createHmac("sha256", SECRET) .update(canonical, "utf8") .digest("base64"); return { "X-Vanta-Key-Id": KEY_ID, "X-Vanta-Timestamp": timestamp, "X-Vanta-Nonce": nonce, "X-Vanta-Signature": `v1=${signature}`, }; } const path = "/api/v1/trading/account"; const response = await fetch(`${BASE_URL}${path}`, { headers: signedHeaders("GET", path), }); console.log(response.status, await response.json());A successful call returns HTTP 200 and the standard envelope:
200 OK{ "success": true, "data": { "accountId": "6f1c2e34-9a4b-4c1d-8e2f-1a2b3c4d5e6f", "assetClass": "crypto", "marketName": "Crypto", "accountSize": 25000, "formattedAccountSize": "25K", "evaluation": { "title": "Crypto 25K Evaluation", "status": "evaluation", "isEliminated": false, "accountSize": "$25,000", "activeAccounts": "1/1", "effectiveAccountSizeNumeric": 25000 }, "account": { "currentBalance": 25120.5, "currentEquity": 25180.25, "balanceChange": 120.5, "balanceChangePercent": 0.48, "totalPnL": 180.25, "totalPnLPercent": 0.72, "openPnL": 59.75, "openPnLPercent": 0.24, "openPositions": 1, "portfolioBalance": 25120.5, "portfolioBalanceChangePercent": 0.48, "portfolioBalanceBreakdown": { "currentBalance": 25120.5, "marginLeverage": 5, "sumPositionValue": 7202.5 }, "leverage": "Current - 0.2870x / Max - 10x", "capitalUsed": 7202.5, "totalRealizedPnl": 120.5, "isPassed": false }, "challenge": { "variant": "default", "bucket": "SUBACCOUNT_CHALLENGE", "drawdownCriteria": "trailing", "profitTarget": 2500, "profitTargetPercent": 10, "remaining": 2319.75, "maxLeverage": "10x", "trailingDrawdownPercent": 5, "maxDrawdown": -1250, "daysRemaining": 27, "totalDays": 90, "drawdownBreakdown": { "highWaterMark": 25180.25, "allowedDrawdown": 1259.01, "currentDrawdown": 0, "remainingDrawdown": 1259.01, "remainingDrawdownPercentHWM": 5 } }, "performance": { "totalTrades": 4, "winRate": 75, "totalWins": 3, "avgTradePnL": 45.06, "tradeDuration": "63h 12m", "challengeStartMs": 1717200000000, "dailyReturns": [ { "date": "2026-06-28", "value": 0.31 }, { "date": "2026-06-29", "value": 0.17 } ] }, "meta": { "generatedAtMs": 1717718400000, "stale": false } } }
Placing your first order
Once a read works, the signing is correct and writes are the same mechanism with a body.
This places a real market order on your account. Size it accordingly the first time, and be ready to close it from the terminal.
# Replace with your key and secret (secret is shown only when you create the key).
export VANTA_KEY_ID="YOUR_KEY_ID"
export VANTA_SECRET="YOUR_SECRET"
export ACCOUNT_ID="YOUR_PROP_ACCOUNT_UUID"
export BASE_URL="https://app.vantatrading.io"
BODY="{\"accountId\":\"YOUR_PROP_ACCOUNT_UUID\",\"trade\":{\"execution_type\":\"MARKET\",\"trade_pair\":\"BTCUSDC\",\"order_type\":\"LONG\",\"value\":1000}}"
PATH_REQ="/api/v1/trading/orders"
TS=$(($(date +%s) * 1000))
NONCE=$(openssl rand -hex 16)
BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | xxd -p -c 256)
CANONICAL=$(printf 'v1\nPOST\n%s\n%s\n%s\n%s' "$PATH_REQ" "$TS" "$NONCE" "$BODY_HASH")
SIG=$(echo -n "$CANONICAL" | openssl dgst -sha256 -hmac "$VANTA_SECRET" -binary | base64 | tr -d '\n')
curl -s -X POST "$BASE_URL$PATH_REQ" \
-H "Content-Type: application/json" \
-H "X-Vanta-Key-Id: $VANTA_KEY_ID" \
-H "X-Vanta-Timestamp: $TS" \
-H "X-Vanta-Nonce: $NONCE" \
-H "X-Vanta-Signature: v1=$SIG" \
-d "$BODY"# pip install requests
import base64
import hashlib
import hmac
import json
import os
import secrets
import time
import requests
# The secret is shown once, when the key is created. Keep it out of source
# control. Read it from the environment or a secret manager.
KEY_ID = os.environ["VANTA_KEY_ID"]
SECRET = os.environ["VANTA_SECRET"]
BASE_URL = "https://app.vantatrading.io"
# The key is bound to one prop account; the body must name that same account.
ACCOUNT_ID = os.environ["VANTA_ACCOUNT_ID"]
def signed_headers(method: str, path: str, body: str = "") -> dict:
"""Build the four auth headers for one request.
`path` must be the request target exactly as it is sent: the pathname plus
the query string, if any. Signing a different string than you request is
the most common cause of a 401.
"""
timestamp = str(int(time.time() * 1000))
nonce = secrets.token_hex(16)
canonical = "\n".join(
[
"v1",
method.upper(),
path,
timestamp,
nonce,
hashlib.sha256(body.encode("utf-8")).hexdigest(),
]
)
signature = base64.b64encode(
hmac.new(
SECRET.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256
).digest()
).decode("ascii")
return {
"X-Vanta-Key-Id": KEY_ID,
"X-Vanta-Timestamp": timestamp,
"X-Vanta-Nonce": nonce,
"X-Vanta-Signature": f"v1={signature}",
}
path = "/api/v1/trading/orders"
# Serialise ONCE, then sign and send those exact bytes. Passing `json=` to
# requests would re-encode the payload after signing and the signature would no
# longer match what arrives. Always send the signed string with `data=`.
body = json.dumps({
"accountId": ACCOUNT_ID,
"trade": {
"execution_type": "MARKET",
"trade_pair": "BTCUSDC",
"order_type": "LONG",
"value": 1000,
},
}, separators=(",", ":"))
headers = signed_headers("POST", path, body)
headers["Content-Type"] = "application/json"
response = requests.post(BASE_URL + path, headers=headers, data=body, timeout=30)
print(response.status_code, response.json())// Node 18 or newer. No dependencies: `fetch` and `node:crypto` are built in.
import { createHash, createHmac, randomBytes } from "node:crypto";
// The secret is shown once, when the key is created. Keep it out of source
// control. Read it from the environment or a secret manager.
const KEY_ID = process.env.VANTA_KEY_ID;
const SECRET = process.env.VANTA_SECRET;
const BASE_URL = "https://app.vantatrading.io";
// The key is bound to one prop account; the body must name that same account.
const ACCOUNT_ID = process.env.VANTA_ACCOUNT_ID;
/**
* Build the four auth headers for one request. `path` must be the request
* target exactly as it is sent: pathname plus query string, if any.
*/
function signedHeaders(method, path, body = "") {
const timestamp = String(Date.now());
const nonce = randomBytes(16).toString("hex");
const canonical = [
"v1",
method.toUpperCase(),
path,
timestamp,
nonce,
createHash("sha256").update(body, "utf8").digest("hex"),
].join("\n");
const signature = createHmac("sha256", SECRET)
.update(canonical, "utf8")
.digest("base64");
return {
"X-Vanta-Key-Id": KEY_ID,
"X-Vanta-Timestamp": timestamp,
"X-Vanta-Nonce": nonce,
"X-Vanta-Signature": `v1=${signature}`,
};
}
const path = "/api/v1/trading/orders";
// Serialise ONCE, then sign and send those exact bytes. Re-stringifying the
// object for the request would change the hash and the signature no longer
// matches what arrives.
const body = JSON.stringify({
"accountId": ACCOUNT_ID,
"trade": {
"execution_type": "MARKET",
"trade_pair": "BTCUSDC",
"order_type": "LONG",
"value": 1000
}
});
const response = await fetch(`${BASE_URL}${path}`, {
method: "POST",
headers: {
...signedHeaders("POST", path, body),
"Content-Type": "application/json",
},
body,
});
console.log(response.status, await response.json());Every execution type, sizing field and bracket option is covered on Placing orders.
If the first call fails
Almost every first-attempt failure is one of these three.
401 Bad signaturemeans the credential was recognised but the HMAC did not match, so the mistake is in what you signed. Check your signer against the worked vectors on Authentication; they tell you which of the canonical string, the body hash or the secret is wrong.401 Timestamp invalidmeans your clock is more than five minutes off, or you signed the request well before sending it.409 Noncemeans the nonce was reused. Generate a fresh random one per request, and never resend a failed request with the same nonce.
The complete list is on Errors & rate limits.