1. Overview
The TradeClick Partner API lets broker partners record trading volume for spread-markup accounting. Each partner authenticates with a server-side API key, syncs the accounts they want tracked, reports executions as they happen (or in bulk), and submits a monthly volume report that TradeClick reconciles against.
This API is for volume tracking and reporting. It does not place, modify, or close trades. TradeClick never sends order instructions through this API — every endpoint here only records data you report to us.
Base URL for every endpoint below: https://tradeclick-api.onrender.com
MT5 brokers — which integration applies to you
If your platform is MT5, two separate things are in play with very different costs. This decides which of them you need to build.
A creator attaches TradeClick_Signal_EA.ex5 to any one chart in their own terminal. It reports every open position to TradeClick every ~2 seconds, and their trades broadcast to their community from there. It authenticates with a TradeClick-issued secret, so the creator never hands over broker credentials, and it is strictly read-only — it contains no order-placing code at all. Works on any MT5 broker, needs no approval or work from you.
| Build the REST API (recommended) | MetaAPI | |
|---|---|---|
| Who places the trade | Your platform | MetaAPI cloud bridge |
| Your build effort | The integration spec | None |
| Ongoing cost | None | Per connected account, ongoing |
| Trader credentials | Never leave your platform | Trader supplies MT5 login + password, held by a third party |
| Account types | Yours to decide | Live only — demo/contest rejected at connect |
| Time to live | After your build | Immediately |
Both give your traders full copy trading — the EA broadcasts signals either way, only the execution leg differs. The REST route is the same model as our production Verity Markets integration. Ask us for current MetaAPI pricing if you want to compare properly.
Why there is no subscriber-side EA. The obvious shortcut — a second EA that receives signals and places them on the subscriber's terminal — is one we deliberately rejected, because it cannot hold the safety contract every other execution path holds.
Everywhere else, placing an order is synchronous and verified in one operation: place, re-read the position from the broker, confirm the protective stop is actually attached, and close the position if it is not. We own that outcome start to finish.
An EA polls. Between "open this" and "here is what happened" sits a poll interval, a network round trip, and a terminal that may be closed, asleep or mid-restart. That gives three failures we will not accept on a funded account:
- We cannot verify a stop is attached. The confirmation is a report from a remote terminal, not a read we performed — and if it stops reporting we cannot tell "no stop" from "no answer".
- We cannot close what we cannot see. A terminal that drops after opening but before protecting leaves a position open, unprotected and invisible to us. Nothing exists to close it.
- The safety logic would run on a machine we do not control, shipped as a compiled binary — so fixing a bug means persuading every subscriber to re-download and re-attach it.
The EA's value is sourcing signals without credentials, and it is very good at that. Execution belongs server-to-server, where whoever places the trade can also guarantee it.
2. Authentication
Authentication is key-exchange-for-session. You exchange your long-lived API key for a short-lived session token once, then use that token on every subsequent call:
POST /v1/b2b/authwith{"api_key":"tc_b2b_live_..."}returns asession_tokenvalid for 15 minutes.- Every other endpoint takes
Authorization: Bearer <session_token>. - When the token expires, call
/v1/b2b/authagain to get a new one.
Your API key is a server-side credential. Never ship it in a browser page, mobile app, or public repository — anything that reaches an end user's device. Keep it in your backend environment only. Keys are stored on our side only as a SHA-256 hash; they can be rotated at any time but can never be recovered or displayed again once issued.
3. Rate limits
Each partner is limited to 120 requests per 60 seconds. Exceeding it returns:
HTTP/1.1 429 Too Many Requests
{
"code": "rate_limited"
}
POST /v1/b2b/accounts/sync additionally accepts a maximum of 100 accounts per call — batch larger books across multiple calls.
4. Endpoints
Exchange your API key for a session token.
{
"api_key": "tc_b2b_live_5f3a9c2e1b7d4a8f9c0e2b1a"
}
{
"ok": true,
"session_token": "sess_9f7a2c4e8b1d6f3a0c5e7b2d4a",
"expires_at": "2026-08-08T14:15:00Z",
"expires_in_s": 900,
"broker": {
"name": "Example Markets",
"slug": "example-markets",
"markup_pips": null,
"markup_scope": "copy_only",
"status": "active"
}
}
curl -X POST https://tradeclick-api.onrender.com/v1/b2b/auth \
-H "Content-Type: application/json" \
-d '{"api_key":"tc_b2b_live_5f3a9c2e1b7d4a8f9c0e2b1a"}'
Verify a session token is still valid and fetch the broker's current markup terms. Use this to check a session before a long-running job, or to refresh cached terms.
No request body.
{
"ok": true,
"broker": {
"name": "Example Markets",
"slug": "example-markets",
"markup_pips": null,
"markup_scope": "copy_only",
"status": "active"
}
}
curl https://tradeclick-api.onrender.com/v1/b2b/ping \ -H "Authorization: Bearer sess_9f7a2c4e8b1d6f3a0c5e7b2d4a"
Register or update the broker accounts you want TradeClick to attribute volume to. Send up to 100 accounts per call.
This records accounts for volume attribution only — it does not create a TradeClick user account, and no login or dashboard access is granted to anyone as a result of this call.
{
"accounts": [
{
"broker_account_id": "10544",
"email": "trader@example.com",
"balance": 5230.11,
"account_currency": "USD",
"status": "active"
}
]
}
{
"ok": true,
"synced": 1,
"skipped": 0
}
curl -X POST https://tradeclick-api.onrender.com/v1/b2b/accounts/sync \
-H "Authorization: Bearer sess_9f7a2c4e8b1d6f3a0c5e7b2d4a" \
-H "Content-Type: application/json" \
-d '{"accounts":[{"broker_account_id":"10544","email":"trader@example.com","balance":5230.11,"account_currency":"USD","status":"active"}]}'
Record a single execution against one of your synced accounts.
Tracking only — TradeClick does not place this trade. This endpoint logs an execution you already carried out on your own platform.
{
"broker_account_id": "10544",
"symbol": "EURUSD",
"side": "buy",
"lots": 0.50,
"entry_price": 1.08423,
"sl": 1.08123,
"tp": 1.09023,
"execution_type": "copy",
"strategy_id": "ttc_sniper",
"broker_ticket": "82251"
}
{
"ok": true,
"recorded": true,
"duplicate": false
}
Passing broker_ticket makes retries idempotent — resending the same ticket returns {"duplicate": true} and is only counted once toward your volume, so it's safe to retry a call that timed out.
curl -X POST https://tradeclick-api.onrender.com/v1/b2b/executions \
-H "Authorization: Bearer sess_9f7a2c4e8b1d6f3a0c5e7b2d4a" \
-H "Content-Type: application/json" \
-d '{"broker_account_id":"10544","symbol":"EURUSD","side":"buy","lots":0.50,"entry_price":1.08423,"sl":1.08123,"tp":1.09023,"execution_type":"copy","strategy_id":"ttc_sniper","broker_ticket":"82251"}'
Submit your monthly trading volume for markup reconciliation.
{
"report_month": "2026-07",
"total_lots": 842.50,
"copy_trading_lots": 610.00,
"manual_lots": 232.50,
"symbol_mix": {
"EURUSD": 420.00,
"XAUUSD": 190.50
}
}
{
"ok": true,
"markup_usd": null,
"markup_lots": 610.00,
"pip_value_basis": "symbol_mix",
"message": "Report received for July 2026."
}
Monetary fields are null in these examples because the rates are specific to your agreement — live responses carry your contract values. Lot counts are your own reported volume and are returned as-is.
symbol_mix is optional — a {"SYMBOL": lots} map (e.g. {"EURUSD":120.5,"XAUUSD":30}) that produces a per-symbol markup instead of applying the default FX-major rate to the whole figure. Omit it if you don't track volume per instrument.
curl -X POST https://tradeclick-api.onrender.com/v1/b2b/volume/report \
-H "Authorization: Bearer sess_9f7a2c4e8b1d6f3a0c5e7b2d4a" \
-H "Content-Type: application/json" \
-d '{"report_month":"2026-07","total_lots":842.50,"copy_trading_lots":610.00,"manual_lots":232.50}'
Returns your last 24 monthly volume reports plus running totals.
No request body.
{
"ok": true,
"reports": [
{
"report_month": "2026-07",
"total_lots": 842.50,
"copy_trading_lots": 610.00,
"markup_lots": 610.00,
"markup_usd": null,
"status": "verified"
}
],
"total_markup_paid_usd": null,
"outstanding_markup_usd": null
}
curl https://tradeclick-api.onrender.com/v1/b2b/volume/history \ -H "Authorization: Bearer sess_9f7a2c4e8b1d6f3a0c5e7b2d4a"
Returns figures derived from the executions you reported via /v1/b2b/executions for the given month. These figures are independent of your monthly volume report — use this to sanity-check your own numbers before submitting one.
No request body. Optional query param: ?month=2026-07 (defaults to the current month).
{
"ok": true,
"month": "2026-07",
"executions": 1284,
"lots_tracked": 842.50,
"copy_lots_tracked": 610.00,
"markup_lots": 610.00,
"markup_pips": null,
"estimated_markup_usd": null,
"pip_value_basis": "fx_major_default",
"note": "Figures are derived from executions you reported via /v1/b2b/executions and are independent of your monthly volume report."
}
markup_pips and estimated_markup_usd are shown as null here only because the rates are specific to your agreement — live responses carry your contract values.
curl "https://tradeclick-api.onrender.com/v1/b2b/stats?month=2026-07" \ -H "Authorization: Bearer sess_9f7a2c4e8b1d6f3a0c5e7b2d4a"
5. Markup calculation
markup_usd = markup_lots × markup_pips × pip_value
Markup rates and pip values are agreed per partnership contract. Contact us for your specific rate structure — the figures applied to your account are the ones in your agreement, and TradeClick computes markup_usd from them server-side on every report.
Pip value is per instrument class, not a single flat number: an FX major, a JPY pair, a metal and an index are each worth a different amount per standard lot. That is why a book weighted toward indices or metals settles differently from a flat FX-major calculation, and why the symbol_mix field below is worth sending.
Without a symbol_mix, the FX-major rate is applied to your whole reported figure, and the response's pip_value_basis field reads fx_major_default. Passing symbol_mix on /v1/b2b/volume/report computes a blended rate per instrument instead, and pip_value_basis reads symbol_mix.
Every response tells you which basis was used. Rather than reproducing a rate card, reconcile against the markup_usd and pip_value_basis TradeClick returns on each report — those are computed from your contract terms and are authoritative. If a figure looks wrong, raise it with us rather than recomputing from an assumed rate.
6. SSO (deep-link handoff)
Deep-link a signed-in trader from your platform straight into a signed-in TradeClick session. A visual walkthrough of this flow (plus a live embed preview) is on the Embed Demo page.
The trader-facing redirect target. Your server signs a short-lived JWT assertion and redirects the trader's browser here; TradeClick validates it and takes them into a sign-in/sign-up flow with their email pre-filled.
{
"iss": "your-slug",
"iat": 1234567890,
"exp": 1234567890 + 120,
"jti": "unique-uuid-per-assertion",
"broker_account_id": "ACC123",
"email": "trader@example.com",
"balance": 5000,
"currency": "USD",
"return_url": "https://app.broker.com/trading"
}
expmust be at most 120 seconds afteriat— anything longer is rejected asinvalid_assertion.jtimust be unique per assertion — it is single-use; a replayedjtiis rejected asassertion_replayed.issmust equal your partner slug exactly.return_urlmust fall under one of your allowlisted origins, or it is silently ignored (the trader lands on the default TradeClick destination instead).
TradeClick does not create an account from your assertion. The trader always authenticates with TradeClick directly (sign in, or sign up with the email pre-filled from your assertion) — their broker account links to their TradeClick account only after that authentication succeeds. A leaked partner secret can pre-fill a form; it can never log someone in on its own.
import time
import uuid
import jwt # pip install pyjwt
# 🔴 SSO_SECRET is a server-side credential — it must NEVER reach browser JS, a mobile
# app bundle, or a public repository. Keep it in your backend environment only.
SSO_SECRET = "your-sso-secret" # issued once by TradeClick; never displayed again
PARTNER_SLUG = "your-slug"
def build_sso_url(trader_email, broker_account_id, balance, currency, return_url):
now = int(time.time())
assertion = {
"iss": PARTNER_SLUG,
"iat": now,
"exp": now + 120, # max 120s
"jti": str(uuid.uuid4()), # single-use
"broker_account_id": broker_account_id,
"email": trader_email,
"balance": balance,
"currency": currency,
"return_url": return_url, # must be under an allowlisted origin
}
token = jwt.encode(assertion, SSO_SECRET, algorithm="HS256")
return f"https://tradeclick.io/v1/b2b/sso?token={token}"
TradeClick admin only. Issues (or rotates) a partner's SSO signing secret. Like the API key, the secret is shown once — store it in your backend environment immediately.
{
"ok": true,
"sso_secret": "sso_5f3a9c2e1b7d4a8f9c0e2b1a...",
"message": "Store this secret now — it will not be shown again."
}
7. Embed
Frame TradeClick's strategy marketplace directly inside your platform. See it live, with a copyable snippet, on the Embed Demo page.
Public — no authentication. Fetched by the /embed page itself (and by the Cloudflare Pages Function in front of it) to learn whether your embed is enabled and which pages/origins are allowed.
{
"ok": true,
"broker": {
"name": "Example Markets",
"slug": "example-markets"
},
"embed_enabled": true,
"allowed_pages": ["marketplace", "strategy", "creator", "help"],
"allowed_origins": ["https://app.example-markets.com"]
}
<iframe src="https://tradeclick.io/embed?broker=YOUR_SLUG&page=marketplace" width="100%" height="600" loading="lazy" title="TradeClick" ></iframe>
Available pages: marketplace (strategy grid), strategy (one strategy — add &slug=), creator (one creator profile — add &username=), help (a compact FAQ, no auth). Only pages listed in your allowed_pages will render for you.
Auth-required actions (Activate / Follow / Copy) always open tradeclick.io in a new tab, by design. The embed runs in a third-party iframe context, where browser storage is commonly partitioned or blocked — TradeClick never attempts a sign-in inside the frame.
Origins must be allowlisted exactly — full scheme + host + port, no wildcards. An unlisted origin gets Content-Security-Policy: frame-ancestors 'self', which browsers will refuse to render in your iframe.
8. Webhooks
TradeClick can POST to your own endpoint whenever a copy trade fires on your side of the integration. This is informational — for your dashboard and reconciliation — not an order feed, and it carries no ordering guarantee between deliveries.
trade.open, trade.close, trade.modify, trade.partial_close, trade.filled — plus test.ping, sent from the TradeClick admin's "Send Test Webhook" button so you can verify your endpoint before going live.
trade.open is a signal. trade.filled is money moving. They are different events on purpose, and if you reconcile volume you almost certainly want the second one.
trade.open | trade.filled | |
|---|---|---|
| Means | A signal was accepted | A position exists at the broker |
| Fires | Once per signal, before execution | Once per fill — one signal across N subscribers sends N events |
| Prices | Requested | ticket + real lots; fill_price when the broker returns one |
| If nothing fills | Still sent | Not sent |
A trade.open can describe a trade that ultimately fills nobody — margin, market hours and risk limits all reject after the signal is accepted. Reconcile executed volume against trade.filled.
trade.filled covers every execution path — TradingView signals, account-level copy trading and strategy copy alike — and you receive only the fills belonging to your own traders.
fill_price may be null. Not every broker returns an execution price on the order response, and we will not substitute the requested price for one — a requested price presented as an execution would quietly corrupt your reconciliation. When it is absent you get null, with the requested level in requested_entry for context. ticket, lots and symbol are always present, and volume-based reconciliation should key on those.
Existing integrations are unaffected. Every event is subscribed per partner and checked on each delivery, so a live integration keeps exactly the traffic it has today. New partners are subscribed to trade.filled from the start; if you are already integrated and want it, ask your TradeClick contact to add it — worth knowing before you do, it is materially more traffic than trade.open, since it is one delivery per subscriber fill rather than one per signal.
| Header | Value |
|---|---|
X-TradeClick-Signature | HMAC signature, see below. |
X-TradeClick-Event | The event type, e.g. trade.open. |
X-TradeClick-Delivery | A stable id for this delivery — same value across retries. Use it to de-duplicate. |
Content-Type | application/json |
User-Agent | TradeClick-Webhooks/1.0 |
The X-TradeClick-Signature header has the format:
t=<unix_timestamp>,v1=<hex_hmac_sha256>
The MAC is computed over the string "<timestamp>.<raw_request_body>", keyed with your webhook signing secret (issued once by the TradeClick admin, alongside your API key).
Verify against the RAW request body bytes — never a re-serialized copy of the parsed JSON. Key order and number formatting can differ from what we sent, and the signature will not match. Read the body before your framework parses it, and compare in constant time (e.g. Python's hmac.compare_digest) — never a plain ==, which leaks timing information. Also reject any delivery whose timestamp is more than ~5 minutes old, to guard against replay.
import hashlib
import hmac
import time
from flask import Flask, request, abort
app = Flask(__name__)
# Issued once by the TradeClick admin alongside your API key — server-side only.
WEBHOOK_SECRET = "your-webhook-signing-secret"
MAX_TIMESTAMP_SKEW_S = 300 # ~5 minutes
def verify_signature(raw_body: bytes, signature_header: str) -> bool:
try:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = parts["t"]
signature = parts["v1"]
except (KeyError, ValueError):
return False
# Reject anything too old — replay protection.
if abs(time.time() - int(timestamp)) > MAX_TIMESTAMP_SKEW_S:
return False
# MAC is over "." — the RAW bytes, never re-serialized JSON.
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(
WEBHOOK_SECRET.encode(), signed_payload, hashlib.sha256
).hexdigest()
# Constant-time comparison — never a plain ==.
return hmac.compare_digest(expected, signature)
@app.route("/tradeclick/webhook", methods=["POST"])
def tradeclick_webhook():
raw_body = request.get_data() # RAW bytes, before Flask parses JSON
signature_header = request.headers.get("X-TradeClick-Signature", "")
if not verify_signature(raw_body, signature_header):
abort(401)
event_type = request.headers.get("X-TradeClick-Event")
delivery_id = request.headers.get("X-TradeClick-Delivery")
payload = request.get_json()
# Return 2xx immediately and process asynchronously — TradeClick does not
# wait for your processing to finish.
enqueue_for_processing(delivery_id, event_type, payload)
return "", 200
const crypto = require("crypto");
const express = require("express");
const app = express();
// Issued once by the TradeClick admin alongside your API key — server-side only.
const WEBHOOK_SECRET = "your-webhook-signing-secret";
const MAX_TIMESTAMP_SKEW_S = 300; // ~5 minutes
function verifySignature(rawBody, signatureHeader) {
const parts = Object.fromEntries(
(signatureHeader || "").split(",").map((p) => p.split("="))
);
const { t: timestamp, v1: signature } = parts;
if (!timestamp || !signature) return false;
// Reject anything too old — replay protection.
const nowS = Math.floor(Date.now() / 1000);
if (Math.abs(nowS - Number(timestamp)) > MAX_TIMESTAMP_SKEW_S) return false;
// MAC is over "." — the RAW bytes, never re-serialized JSON.
const signedPayload = Buffer.concat([
Buffer.from(`${timestamp}.`),
rawBody,
]);
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(signedPayload)
.digest("hex");
// Constant-time comparison — never a plain ===.
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
// express.raw() keeps the body as a Buffer of the exact bytes we sent —
// express.json() would re-parse it first and the raw bytes needed for the
// signature check would already be gone.
app.post(
"/tradeclick/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const signatureHeader = req.headers["x-tradeclick-signature"];
if (!verifySignature(req.body, signatureHeader)) {
return res.sendStatus(401);
}
const eventType = req.headers["x-tradeclick-event"];
const deliveryId = req.headers["x-tradeclick-delivery"];
const payload = JSON.parse(req.body);
// Return 2xx immediately and process asynchronously — TradeClick does
// not wait for your processing to finish.
enqueueForProcessing(deliveryId, eventType, payload);
res.sendStatus(200);
}
);
{
"event": "trade.open",
"timestamp": "2026-08-08T14:12:03Z",
"strategy": "ttc_sniper",
"symbol": "EURUSD",
"side": "buy",
"lots": 0.50,
"entry_price": 1.08423,
"sl": 1.08123,
"tp": 1.09023,
"signal_id": "sig_9f7a2c4e8b1d6f3a",
"subscriber_count": 42,
"successful_copies": 39
}
{
"event": "trade.filled",
"timestamp": "2026-08-08T14:12:07Z",
"strategy": "ttc_sniper",
"broker": "verity",
"ticket": "102083",
"symbol": "EUR/USD",
"broker_symbol": "EURUSD",
"side": "buy",
"lots": 0.01,
"fill_price": 1.08431,
"requested_entry": 1.08423,
"sl": 1.08123,
"tp": 1.09023,
"signal_id": "sig_9f7a2c4e8b1d6f3a",
"account_id": "58e2f24e-77a2-4248-a615-52452b1eca47"
}
ticket is the broker-side position identifier, so it reconciles directly against your own records. symbol is the TradeClick pair; broker_symbol is the instrument as your platform named it (e.g. EURUSD.pro) — match on that. strategy may be copy:<creator> for account-level copy trading, which has no marketplace strategy of its own. grid_no_stop: true appears on grid fills that intentionally carry no stop loss.
A delivery is retried up to 5 times with backoff 30s → 2m → 10m → 30m → 2h, then marked dead. Any 2xx response is treated as acknowledged. A non-2xx response, a timeout (10s), or a connection error all trigger a retry.
Return 2xx fast and process asynchronously. TradeClick does not wait for your processing to finish — acknowledge the delivery immediately, then do your own work off the request.
- HTTPS only.
- Your endpoint must resolve to a public address — loopback, private, and link-local addresses are rejected.
- Redirects are not followed. Point the URL directly at your final endpoint.
X-TradeClick-Delivery is stable across retries of the same delivery — de-duplicate your processing on it. Delivery is at-least-once, not exactly-once, so the same delivery id can legitimately arrive more than once.
8b. Configuring webhooks yourself
You can set your own endpoint, choose which events you receive, and generate your own signing secret — from the Partner Portal or these endpoints. All three are authenticated by your session token, exactly like the rest of Section 4.
Commercial terms are not settable here. Markup rate, scope and account status are agreed in your contract and can only be changed by TradeClick. These endpoints cover delivery mechanics only. API-key rotation is also not self-service — rotating without simultaneously updating your caller would lock you out of your own integration, so ask us and we will coordinate it.
Your current webhook configuration.
{
"ok": true,
"webhook_url": "https://your-platform.com/webhooks/tradeclick",
"webhook_enabled": true,
"events": ["trade.filled", "trade.close"],
"available_events": ["trade.open", "trade.close", "trade.modify",
"trade.partial_close", "trade.filled", "test.ping"],
"secret_prefix": "tc_whsec_9f7a2c4e",
"secret_set": true,
"algorithm": "HMAC-SHA256",
"signature_header": "X-TradeClick-Signature"
}The signing secret is never returned. Only secret_prefix is, which is enough to tell two secrets apart and useless for forging a signature. If you lose the secret, generate a new one — there is no recovery path by design.
Set your endpoint, event subscription, or delivery on/off. Send only the fields you want to change.
| Field | Type | Notes |
|---|---|---|
webhook_url | string | Optional. https only, must resolve to a public address. Send "" to clear it, which also disables delivery. |
events | array | Optional. Replaces your subscription. Any value outside available_events is rejected. |
enabled | boolean | Optional. Cannot be true without an endpoint URL. |
{
"webhook_url": "https://your-platform.com/webhooks/tradeclick",
"events": ["trade.filled", "trade.close"],
"enabled": true
}curl -X PATCH https://tradeclick-api.onrender.com/v1/b2b/webhook \
-H "Authorization: Bearer sess_9f7a2c4e8b1d6f3a0c5e7b2d4a" \
-H "Content-Type: application/json" \
-d '{"webhook_url":"https://your-platform.com/webhooks/tradeclick","enabled":true}'Your URL is validated before it is stored, and again before every delivery. It must be https and resolve to a publicly-routable address — loopback, private ranges and link-local (including cloud metadata at 169.254.169.254) are all rejected with 400 invalid_webhook_url. Deliveries do not follow redirects, so a 302 to an internal host will not be followed.
Generate your signing secret. Returned once.
{
"ok": true,
"webhook_secret": "tc_whsec_…",
"prefix": "tc_whsec_9f7a2c4e",
"algorithm": "HMAC-SHA256",
"signature_header": "X-TradeClick-Signature"
}Generating a new secret immediately invalidates the previous one. Deliveries will fail signature verification until the new value is installed on your server, so deploy it before or immediately after generating. See Section 8 for the verification recipe.
8c. Trader management
View, activate and deactivate copy trading for your traders from your own platform.
The one-time setup. Each trader completes a single TradeClick account setup through the SSO handoff before you can manage their copy trading here. It takes under a minute and happens once per trader — after that everything below is available from your API.
That step is not a formality we can skip for you. Placing orders needs live broker credentials that only the trader can supply, and TradeClick accounts are created by the person who owns them — we never mint one on a trader's behalf. The SSO flow is what turns your broker_account_id into a managed trader; until then these endpoints return 404 trader_not_linked.
Strategies currently running for one of your traders.
{
"ok": true,
"broker_account_id": "8891234",
"account_connected": true,
"strategies": [
{
"slug": "ttc_sniper",
"name": "TTC Sniper",
"symbol": "XAUUSD",
"status": "running",
"started_at": "2026-08-09T09:14:02Z",
"risk_percent": 1.0,
"sizing_mode": "risk_pct"
}
]
}account_connected: false with an empty list means the trader finished setup but has not connected a broker account yet. risk_percent is null when the trader sizes by fixed lot — reporting 0 there would read as "no risk".
Start a strategy on the trader's connected account.
{
"ack": true,
"risk_percent": 1.0,
"pair": "any"
}| Field | Notes |
|---|---|
ack | Required, must be true. Confirms this trader has been shown the risk disclosure. Recorded against your partner account. |
risk_percent | Optional. Per-trade risk. Validated against the same limits the trader-facing app enforces. |
pair | Optional. A symbol, or any. Omitted uses the strategy's default. |
This runs every check the trader-facing app runs, against the trader's own account: strategy availability, IB attribution, funding against that strategy's minimum, and the three-strategies-per-account cap. A request can therefore fail for reasons specific to that trader — the response body names which.
| Status | Code | Meaning |
|---|---|---|
| 400 | risk_acknowledgement_required | ack was not true |
| 403 | account_not_connected | trader has no broker account connected |
| 403 | deposit_gate_blocked | balance below this strategy's minimum |
| 404 | trader_not_linked | one-time setup not completed |
| 409 | strategy_limit_reached | already running three strategies |
Stop a strategy for a trader. No gates — stopping is always permitted.
{
"ok": true,
"slug": "ttc_sniper",
"stopped": 1,
"positions_closed": false
}Open positions are NOT closed. This stops new signals from executing; anything already in the market keeps its stop-loss and take-profit and exits on its own terms. Closing them here would turn a configuration change into a forced liquidation at whatever price happens to be showing. To flatten a position, the trader closes it from their own account.
9. Code examples
A full flow — authenticate, then submit a monthly volume report. Run both from your own backend; never from a browser page, since the API key must never reach an end user's device.
import requests
BASE_URL = "https://tradeclick-api.onrender.com"
API_KEY = "tc_b2b_live_5f3a9c2e1b7d4a8f9c0e2b1a" # server-side secret — keep out of version control
# 1. Exchange the API key for a short-lived session token.
auth = requests.post(f"{BASE_URL}/v1/b2b/auth", json={"api_key": API_KEY})
auth.raise_for_status()
session_token = auth.json()["session_token"]
headers = {"Authorization": f"Bearer {session_token}"}
# 2. Submit the monthly volume report.
report = requests.post(
f"{BASE_URL}/v1/b2b/volume/report",
headers=headers,
json={
"report_month": "2026-07",
"total_lots": 842.50,
"copy_trading_lots": 610.00,
"manual_lots": 232.50,
},
)
report.raise_for_status()
print(report.json())
const BASE_URL = "https://tradeclick-api.onrender.com";
const API_KEY = "tc_b2b_live_5f3a9c2e1b7d4a8f9c0e2b1a"; // server-side secret — never ship this in a browser
async function reportVolume() {
// 1. Exchange the API key for a short-lived session token.
const authRes = await fetch(`${BASE_URL}/v1/b2b/auth`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: API_KEY }),
});
if (!authRes.ok) throw new Error(`auth failed: ${authRes.status}`);
const { session_token } = await authRes.json();
// 2. Submit the monthly volume report.
const reportRes = await fetch(`${BASE_URL}/v1/b2b/volume/report`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session_token}`,
},
body: JSON.stringify({
report_month: "2026-07",
total_lots: 842.5,
copy_trading_lots: 610.0,
manual_lots: 232.5,
}),
});
if (!reportRes.ok) throw new Error(`report failed: ${reportRes.status}`);
console.log(await reportRes.json());
}
reportVolume().catch(console.error);
10. Errors
Errors are returned as {"code": "..."} (sometimes with a human-readable message) alongside a matching HTTP status.
| Code | Meaning |
|---|---|
invalid_api_key | The API key sent to /v1/b2b/auth was not recognised. |
revoked_api_key | The key has been rotated or revoked and can no longer authenticate. |
missing_session | No Authorization: Bearer header was sent. |
invalid_session | The session token is malformed or unknown. |
session_expired | The session token has passed its 15-minute expiry — call /v1/b2b/auth again. |
broker_account_suspended | The broker account tied to this key has been suspended. |
rate_limited | More than 120 requests were sent in a 60-second window. |
invalid_side | side on an execution was not "buy" or "sell". |
invalid_lots | lots was missing, zero, or negative. |
invalid_month_format | report_month was not in YYYY-MM format. |
inconsistent_lots | copy_trading_lots and manual_lots don't reconcile against total_lots. |
report_already_paid | That month's report has already been verified and paid, and is now frozen. |
too_many_accounts | More than 100 accounts were sent to /v1/b2b/accounts/sync in one call. |
sso_not_enabled | SSO has not been enabled for this partner yet. |
invalid_assertion | The SSO token's signature, iss, or shape was invalid. |
assertion_expired | The SSO token's exp has passed (assertions are valid for at most 120 seconds). |
assertion_replayed | The SSO token's jti has already been used — assertions are single-use. |
unknown_partner | The broker slug in iss (SSO) or broker (embed) does not match a known partner. |
embed_not_enabled | Embedding has not been enabled for this partner yet. |
page_not_allowed | The requested &page= is not in this partner's allowed_pages. |
webhook_url_required_https | Your execution_webhook_url must be an https:// address. |
webhook_url_not_public | Your webhook URL must resolve to a public address — loopback, private, and link-local addresses are rejected. |
webhook_not_enabled | Webhooks have not been enabled for this partner, or no execution_webhook_url is configured yet. |
webhook_delivery_failed | The delivery exhausted all 5 retry attempts (30s → 2m → 10m → 30m → 2h) without a 2xx response, and was marked dead. |
11. Support
Questions about integrating, a key you need rotated, or a report that looks wrong — email partners@tradeclick.io and include your broker slug where relevant.