Partner API reference

TradeClick Partner API

Volume tracking and reporting for broker partners — how to authenticate, sync accounts, log executions, and report monthly volume.

v1 Base URL https://tradeclick-api.onrender.com Get API Access

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.

Creator side — already solved, nothing 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.

Subscriber copy execution — pick one
Build the REST API (recommended)MetaAPI
Who places the tradeYour platformMetaAPI cloud bridge
Your build effortThe integration specNone
Ongoing costNonePer connected account, ongoing
Trader credentialsNever leave your platformTrader supplies MT5 login + password, held by a third party
Account typesYours to decideLive only — demo/contest rejected at connect
Time to liveAfter your buildImmediately

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:

  1. POST /v1/b2b/auth with {"api_key":"tc_b2b_live_..."} returns a session_token valid for 15 minutes.
  2. Every other endpoint takes Authorization: Bearer <session_token>.
  3. When the token expires, call /v1/b2b/auth again 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

POST/v1/b2b/auth

Exchange your API key for a session token.

Request
{
  "api_key": "tc_b2b_live_5f3a9c2e1b7d4a8f9c0e2b1a"
}
Response
{
  "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
curl -X POST https://tradeclick-api.onrender.com/v1/b2b/auth \
  -H "Content-Type: application/json" \
  -d '{"api_key":"tc_b2b_live_5f3a9c2e1b7d4a8f9c0e2b1a"}'
GET/v1/b2b/ping

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.

Request
No request body.
Response
{
  "ok": true,
  "broker": {
    "name": "Example Markets",
    "slug": "example-markets",
    "markup_pips": null,
    "markup_scope": "copy_only",
    "status": "active"
  }
}
curl
curl https://tradeclick-api.onrender.com/v1/b2b/ping \
  -H "Authorization: Bearer sess_9f7a2c4e8b1d6f3a0c5e7b2d4a"
POST/v1/b2b/accounts/sync

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.

Request
{
  "accounts": [
    {
      "broker_account_id": "10544",
      "email": "trader@example.com",
      "balance": 5230.11,
      "account_currency": "USD",
      "status": "active"
    }
  ]
}
Response
{
  "ok": true,
  "synced": 1,
  "skipped": 0
}
curl
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"}]}'
POST/v1/b2b/executions

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.

Request
{
  "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"
}
Response
{
  "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
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"}'
POST/v1/b2b/volume/report

Submit your monthly trading volume for markup reconciliation.

Request
{
  "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
  }
}
Response
{
  "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
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}'
GET/v1/b2b/volume/history

Returns your last 24 monthly volume reports plus running totals.

Request
No request body.
Response
{
  "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
curl https://tradeclick-api.onrender.com/v1/b2b/volume/history \
  -H "Authorization: Bearer sess_9f7a2c4e8b1d6f3a0c5e7b2d4a"
GET/v1/b2b/stats?month=YYYY-MM

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.

Request
No request body. Optional query param: ?month=2026-07 (defaults to the current month).
Response
{
  "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
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.

GET/v1/b2b/sso?token=<signed-jwt>

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.

Assertion format (HS256, signed with your SSO secret)
{
  "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"
}
Rules
  • exp must be at most 120 seconds after iat — anything longer is rejected as invalid_assertion.
  • jti must be unique per assertion — it is single-use; a replayed jti is rejected as assertion_replayed.
  • iss must equal your partner slug exactly.
  • return_url must 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.

Python signing example (pyjwt) — server-side ONLY
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}"
POST/v1/admin/b2b/partners/{id}/sso-secret

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.

Response
{
  "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.

GET/v1/b2b/embed-config?broker=<slug>

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.

Response
{
  "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 snippet
<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.

Events

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.opentrade.filled
MeansA signal was acceptedA position exists at the broker
FiresOnce per signal, before executionOnce per fill — one signal across N subscribers sends N events
PricesRequestedticket + real lots; fill_price when the broker returns one
If nothing fillsStill sentNot 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.

Headers we send
HeaderValue
X-TradeClick-SignatureHMAC signature, see below.
X-TradeClick-EventThe event type, e.g. trade.open.
X-TradeClick-DeliveryA stable id for this delivery — same value across retries. Use it to de-duplicate.
Content-Typeapplication/json
User-AgentTradeClick-Webhooks/1.0
Signature verification

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.

Python (Flask) example
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
Node (Express) example
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);
  }
);
Example payload — trade.open
{
  "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
}
Example payload — trade.filled
{
  "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.

Retries

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.

Requirements
  • 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.
Idempotency

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.

GET/v1/b2b/webhook

Your current webhook configuration.

Response
{
  "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.

PATCH/v1/b2b/webhook

Set your endpoint, event subscription, or delivery on/off. Send only the fields you want to change.

Request
FieldTypeNotes
webhook_urlstringOptional. https only, must resolve to a public address. Send "" to clear it, which also disables delivery.
eventsarrayOptional. Replaces your subscription. Any value outside available_events is rejected.
enabledbooleanOptional. Cannot be true without an endpoint URL.
Request
{
  "webhook_url": "https://your-platform.com/webhooks/tradeclick",
  "events": ["trade.filled", "trade.close"],
  "enabled": true
}
curl
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.

POST/v1/b2b/webhook/secret

Generate your signing secret. Returned once.

Response
{
  "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.

GET/v1/b2b/traders/{broker_account_id}/strategies

Strategies currently running for one of your traders.

Response
{
  "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".

POST/v1/b2b/traders/{broker_account_id}/strategies/{slug}/activate

Start a strategy on the trader's connected account.

Request
{
  "ack": true,
  "risk_percent": 1.0,
  "pair": "any"
}
FieldNotes
ackRequired, must be true. Confirms this trader has been shown the risk disclosure. Recorded against your partner account.
risk_percentOptional. Per-trade risk. Validated against the same limits the trader-facing app enforces.
pairOptional. 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.

Errors
StatusCodeMeaning
400risk_acknowledgement_requiredack was not true
403account_not_connectedtrader has no broker account connected
403deposit_gate_blockedbalance below this strategy's minimum
404trader_not_linkedone-time setup not completed
409strategy_limit_reachedalready running three strategies
DELETE/v1/b2b/traders/{broker_account_id}/strategies/{slug}

Stop a strategy for a trader. No gates — stopping is always permitted.

Response
{
  "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.

Python (requests)
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())
JavaScript (fetch, Node/server-side)
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.

CodeMeaning
invalid_api_keyThe API key sent to /v1/b2b/auth was not recognised.
revoked_api_keyThe key has been rotated or revoked and can no longer authenticate.
missing_sessionNo Authorization: Bearer header was sent.
invalid_sessionThe session token is malformed or unknown.
session_expiredThe session token has passed its 15-minute expiry — call /v1/b2b/auth again.
broker_account_suspendedThe broker account tied to this key has been suspended.
rate_limitedMore than 120 requests were sent in a 60-second window.
invalid_sideside on an execution was not "buy" or "sell".
invalid_lotslots was missing, zero, or negative.
invalid_month_formatreport_month was not in YYYY-MM format.
inconsistent_lotscopy_trading_lots and manual_lots don't reconcile against total_lots.
report_already_paidThat month's report has already been verified and paid, and is now frozen.
too_many_accountsMore than 100 accounts were sent to /v1/b2b/accounts/sync in one call.
sso_not_enabledSSO has not been enabled for this partner yet.
invalid_assertionThe SSO token's signature, iss, or shape was invalid.
assertion_expiredThe SSO token's exp has passed (assertions are valid for at most 120 seconds).
assertion_replayedThe SSO token's jti has already been used — assertions are single-use.
unknown_partnerThe broker slug in iss (SSO) or broker (embed) does not match a known partner.
embed_not_enabledEmbedding has not been enabled for this partner yet.
page_not_allowedThe requested &page= is not in this partner's allowed_pages.
webhook_url_required_httpsYour execution_webhook_url must be an https:// address.
webhook_url_not_publicYour webhook URL must resolve to a public address — loopback, private, and link-local addresses are rejected.
webhook_not_enabledWebhooks have not been enabled for this partner, or no execution_webhook_url is configured yet.
webhook_delivery_failedThe 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.

Broker Partnerships Embed Demo Partner Portal partnerships@tradeclick.io
TradeClick Technologies · Partner API · 2026