Webhook API · version 1.0

Webhook API Integration Guide

Signal providers deliver signals to trade2sync by posting JSON to a webhook URL issued by a subscriber. Each accepted signal is parsed and executed across the subscriber's connected broker accounts. The contract is one endpoint, one JSON body and one stable message identifier.

JSON over HTTPS No SDK to install
a standard request
POST https://webhook.trade2sync.com/wh/WEBHOOK_TOKEN
Content-Type: application/json

{
  "message_id": 1001,
  "message": "BUY XAUUSD @ 2350 SL 2345 TP 2360"
}

01. How it works

A trade2sync user — the subscriber — creates a webhook in the trade2sync dashboard, copies the URL it generates, and passes that URL to the signal provider. From then on, every signal the provider posts to that URL is parsed and executed on the subscriber's broker accounts automatically.

Each webhook URL belongs to exactly one subscriber. Signals posted to it are not shared with anyone else, and the provider never has visibility of the subscriber's accounts, brokers or position sizes. The provider publishes; the subscriber's own configuration decides how each signal is traded.

TermMeaning in this guide
ProviderThe party sending signals — a signal service, trading platform, bot or alerting tool. This guide is written for providers.
SubscriberThe trade2sync user who created the webhook and whose broker accounts execute the signals.
There is nothing to install

No SDK, library or trade2sync account is needed on the provider side. Any system that can make an HTTPS POST with a JSON body can integrate.

02. Quickstart

Given a webhook URL and nothing else, a working integration takes a few minutes.

  1. Post a first signal

    Paste the issued URL in place of the placeholder and run the request. Plain text on its own is a valid signal, so this is the shortest thing that works:

    curl — plain text
    curl -X POST https://webhook.trade2sync.com/wh/WEBHOOK_TOKEN \
      -d 'BUY XAUUSD @ 2350 SL 2345 TP 2360'

    Sending JSON instead adds message_id, which enables duplicate protection, corrections and trade management. Every production integration should send it:

    curl
    curl -X POST https://webhook.trade2sync.com/wh/WEBHOOK_TOKEN \
      -H "Content-Type: application/json" \
      -d '{"message_id": 1001, "message": "BUY XAUUSD @ 2350 SL 2345 TP 2360"}'

    A successful response (status code 200):

    200 OK
    {"status": "accepted"}
  2. Add the credential, if one was issued

    If the subscriber supplied a secret, a username and password, or a header value, add the matching header as described under Authentication. If only a URL was supplied, no further setup is needed.

  3. Send real signals with stable identifiers

    Use the provider platform's own message identifier as message_id — the value it already assigns to each published signal — and keep it identical across retries. Acknowledgment response should be read before going live; it is the section that prevents most support tickets.

03. What the subscriber supplies

The subscriber copies the values below out of the trade2sync dashboard and passes them to the provider directly. trade2sync is not involved in that handover; anything that appears to be missing should be requested from the subscriber.

Every integration receives a webhook URL. Depending on which security option was enabled when the webhook was created, one credential may accompany it — never more than one:

Supplied by the subscriberRequired action
A URL and nothing elsePost to it. No headers needed. See No authentication
A URL, a username and a passwordSend an Authorization header. See Basic
A URL and a key valueSend it in a header. See Custom header
A URL and a secretSign each request. See Signature
The URL is a credential in its own right

Anyone holding the URL can send signals to the subscriber's accounts. It should be stored like an API key — outside source control, screenshots and support conversations. This matters most when the URL is the only thing that was issued.

04. Authentication

Each webhook enforces exactly one method, chosen by the subscriber when the webhook was created. The provider does not select or change it — the method in use is whichever matches the credential that was supplied. When in doubt, send an unsigned request: a 401 status code means a credential is required; a 200 status code means none is.

4.1 No authentication

When all security options are left switched off, the webhook is created in none mode. There is no header to send and no credential to store — possession of the URL is the authorisation. The body is posted and nothing else.

This mode exists because many signal platforms — TradingView alerts, Zapier, Make, n8n and other no-code tools — cannot compute a signature or attach a custom header.

A webhook in this mode never returns a 401 status code

It cannot distinguish a genuine signal from anyone else's. The URL must be treated as the secret it is; if it may have leaked, the subscriber should regenerate the webhook, which issues a new URL and invalidates the old one immediately.

4.2 Basic

The issued username has the form whk_1a2b3c4d. Nothing needs to be encoded. All three of the forms below are accepted, so whichever one a platform can produce will work:

all three are accepted
# 1. standard base64 — what most HTTP clients build automatically
Authorization: Basic d2hrXzFhMmIzYzRkOndlYmhvb2tfcGFzc3dvcmQ=

# 2. plain, with the Basic prefix — no encoding step
Authorization: Basic whk_1a2b3c4d:webhook_password

# 3. plain, with no prefix at all
Authorization: whk_1a2b3c4d:webhook_password

This exists for platforms with a single header field and no way to base64-encode. Security is unchanged across all three: comparison is constant-time, a blank credential fails closed, and a wrong password still returns a 401 status code in every form.

In curl, -u user:pass produces form 1 automatically. Basic authentication identifies the sender but, unlike a signature, does not protect the body itself.

4.3 Custom header

The issued key value is sent in a header named exactly X-Webhook-Key. The header name is fixed; only the value is specific to the webhook.

header
X-Webhook-Key: the-issued-key-value

Suitable for platforms that can attach a static header but cannot compute a per-request signature.

4.4 Signature

The recommended method, and the only one that proves the body was not altered in transit. Compute an HMAC-SHA256 over the raw request body using the issued secret, hex-encode it in lowercase, and send it in the X-Signature header.

DetailValue
HeaderX-Signature
AlgorithmHMAC-SHA256
EncodingLowercase hex, no prefix — not sha256=, not base64, not uppercase
Signed overThe exact bytes of the request body
Serialise once, then sign and send the same bytes

The signature covers the raw body exactly as it arrives. Build the JSON string, sign that string, and send that string. Signing an object and letting the HTTP library re-serialise it can change key order or whitespace, which produces a different body and a guaranteed 401 status code. This is the most common integration failure, and it presents as a credential problem when it is not one.

python
import hmac, hashlib, json

body = json.dumps({
    "message_id": 1001,
    "message": "BUY XAUUSD @ 2350 SL 2345 TP 2360",
}, separators=(",", ":")).encode()

signature = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
# send `body` itself — never re-encode the dict

05. The request body

Any body is accepted. A JSON object is read field by field, as described below. Plain text, or anything that is not valid JSON, becomes the signal text verbatim — nothing else is required.

Plain text works on its own

A body of BUY XAUUSD @ 2350 SL 2345 TP 2360, sent with no JSON and no fields, is a valid signal. Alert platforms that can only send plain text work without any wrapper.

Only two bodies are rejected: a completely empty one, and a JSON object with no message key.

When sending JSON, these are the fields that are read:

FieldTypeDescription
messagestringrequiredThe signal text, exactly as published to a human audience. May be "" only when an image is attached
message_idintegeressentialThe provider's own identifier for the signal. Not enforced by validation, but it is the key for duplicate protection, corrections and trade management, so every production integration should send it. Must be unique within the stream and never reused
dateintegeroptionalWhen the signal was published, as unix time in seconds. Used for the 24-hour age check on new entries; when omitted, the signal is processed as current
reply_to_message_idintegeroptionalThe message_id this message updates. See Managing a trade
edit_countintegeroptionalDefaults to 0. Incremented to correct a signal that was already sent
mediaobjectoptionalA base64 image. See Images
channel_idoptionaloptionalAccepted and stored if any; but system will use the webhook id as channel id, because the URL already identifies the channel
channel_nameoptionaloptionalAccepted and stored if any; but system will use the webhook name as channel name, because the URL already identifies the channel
message_id is optional to the validator, essential to the integration

A request without message_id is accepted, but nothing about it can be recognised later. Duplicate protection is keyed on it, so without one, a retry is treated as a brand-new signal and can open a second position — and there is no identifier with which to correct or manage the trade afterwards. Platforms that retry on timeout make this likely rather than theoretical. The provider platform's own message id should be sent on every request.

date is seconds, not milliseconds

A JavaScript Date.now() returns milliseconds and is a thousand times too large. Use Math.floor(Date.now() / 1000). A millisecond timestamp lands far in the future and the signal will not execute.

5.1 Write signals for a parser, not for a person

The message text is read by a parser. It handles the formats signal providers commonly use — emoji, line breaks, multiple take-profits — but the essentials must be present in the text:

  • The instrument: XAUUSD, EURUSD, NAS100
  • The direction: buy or sell
  • Entry, stop loss and take-profit levels, where they apply

A consistent format between signals parses more reliably than a varied one.

06. Images Signal Message

Chart screenshots can be attached as base64 inside the body. There is no multipart upload.

json
{
  "message_id": 1002,
  "message": "BUY XAUUSD @ 2350 SL 2345 TP 2360",
  "media": {
    "type": "photo",
    "data": "iVBORw0KGgoAAAANSUhEUgAA..."
  }
}
RuleLimit
FormatsPNG or JPEG
Decoded image size5 MB maximum
Whole request7 MB maximum — base64 adds roughly a third
EncodingStandard base64. No data:image/png;base64, prefix
media.typeUse "photo"

An image-only signal is allowed: set message to "" and attach the media.

Send the text too, when it exists

When a signal exists as both a chart and a line of text, both should be sent. Text is unambiguous, arrives faster and gives the parser a direct read; the image is best treated as a supplement rather than the whole signal.

07. Corrections

To correct a signal that was already sent — a mistyped entry, a revised stop — resend it with the same message_id and edit_count increased by one.

Sequencemessage_idedit_countResult
Original10010Processed
Same message sent twice10010Ignored as a duplicate — safe
Correction10011Processed as an update
Second correction10012Processed as an update
Keep edit_count stable across network retries

If a request times out and is resent, the identical edit_count must be sent. Incrementing it on a retry turns a duplicate into a correction, and the subscriber's position is modified when nothing actually changed.

08. Managing a trade

Closing, moving a stop, or taking partials is a new message, not an edit. There are two ways to tie it to the original signal.

8.1 Reply to the original

Set reply_to_message_id to the message_id of the signal being managed. This is the reliable method — it is explicit and cannot be misread.

json
{
  "message_id": 1003,
  "message": "Close half, move SL to break even",
  "reply_to_message_id": 1001
}

8.2 Use a management phrase

A message containing any of these is recognised as managing an existing trade even without a reply:

close modify move sl sl to break even breakeven cancel partial close half

Management messages are never too late

The age limit that applies to new entries does not apply here. A close sent hours after the entry still executes, so an open position can always be exited.

09. Acknowledgment response

This is the most important section in this guide. A status code of 200 means trade2sync received and authenticated the request. It does not mean a trade was placed.

The API responds immediately and processes afterwards, so a request never waits on a broker. That keeps delivery fast, but it means a signal can still stop after a status code of 200 has been returned.

Where a signal goes after the response
Receipt and authenticationThis is what the status code of 200 confirms. Everything below happens after the response has been sent.
!
The body is readOnly an empty body, or a JSON object with no message key, stops here. Plain text passes straight through.
!
Duplicates are filteredA repeated message_id and edit_count stops here, deliberately.
!
Age is checkedNew entries with a date older than 24 hours stop here. Management messages skip this check.
!
The text is parsedText the parser cannot resolve into an instrument and a direction stops here.
The subscriber's own rules applyFilters, lot sizing and risk limits decide the rest. That part belongs to the subscriber, not the provider.

So a status code of 200 means "correctly delivered" — the part the provider controls. If a signal was accepted but produced no trade, the cause is one of the steps above, and the subscriber can see which one in the trade2sync dashboard.

When a subscriber reports that a signal did not fire

Three checks explain nearly every case: was message_id genuinely new, was date (if sent) in seconds and recent, and did the text name an instrument and a direction.

10. Response status codes

Every response is JSON. Only two status codes are worth retrying.

200
Accepted

Received and authenticated. Includes duplicates — a repeat is accepted and then filtered.

{"status": "accepted"}
Move on
401
Authentication failed

Wrong, missing or malformed credential. Retrying fails identically — the credential must be fixed first. With signatures, confirm that the exact bytes sent are the bytes signed.

{"detail": "Authentication failed."}
Don't retry
404
Webhook not available

The URL is unknown, or the subscriber has switched the webhook off or deleted it. Stop sending and contact the subscriber. The endpoint can stay configured — if the webhook is switched back on, the same URL resumes working.

{"detail": "Not found."}
Don't retry
429
Rate limited

No webhook limit has been set for the provider. A Retry-After header states how many seconds to wait.

{"detail": "Request was throttled. Expected available in 30 seconds."}
Retry
503
Temporarily unavailable

The signal could not be queued. The request itself was fine — retry it with backoff.

{"detail": "Temporarily unavailable."}
Retry
405
Wrong method

The endpoint accepts POST only. Opening the URL in a browser issues a GET and returns this — it is not a sign that the webhook is broken.

{"detail": "Method \"GET\" not allowed."}
Use POST

11. Retries and rate limits

11.1 The limit

No limits, counted per webhook URL. Well above any realistic signal rate — it takes two requests a second to reach it.

Failed attempts count too

Rejected requests consume the allowance. A misconfigured integration retrying a 401 status code in a tight loop rate-limits itself, and the resulting 429 status code masks the original credential problem. A 401 status code should be fixed, not retried.

11.2 Retrying safely

Duplicate protection means an accidental resend is harmless — provided nothing about the message changes:

  • Same message_id. A new id on a retry creates a second position at double the intended risk. This is the mistake that costs the subscriber real money.
  • Same edit_count. Incrementing turns a retry into a correction.
  • Same date. The original publication time is kept, not the retry time.
  • Backoff. A few seconds, doubling, capped at a minute. Honour Retry-After when it is present.

Only status codes 429 and 503 are worth retrying. A 401 or 404 status code returns the same answer every time.

12. Why a signal stops

Split into what the provider can fix and what belongs to the subscriber.

12.1 Provider-side causes

What went wrongThe fix
Completely empty bodySend the signal text, or a JSON object containing message
JSON object with no message keyAdd message, or send the text as a plain body instead
message_id sent as a stringSend integers — 1001, not "1001"
Negative or non-integer edit_countZero or a non-negative number
Image over 5 MB or unreadableCompress it; send valid PNG or JPEG
Entry older than 24 hours (if provider sends date in payload)Send at publication time, and confirm date is in seconds
Text names no instrument or directionMake the pair and the side explicit

12.2 Subscriber-side causes

  • The webhook is switched off or deleted. The request receives a 404 status code. Only the subscriber can re-enable it.
  • The subscriber's filters rejected it. Trading hours, news filters, per-channel rules. Working as intended, and not visible to the provider.
  • A duplicate was filtered. If that was unintentional, a message_id is being reused.
The 24-hour rule applies to entries only

A new entry whose date is more than 24 hours old is skipped, on the reasoning that a day-old entry is history rather than a trade. Closes, modifications and other management messages are never age-limited.

13. Testing safely

A live URL fires real trades on the first request

There is no dry-run mode that can be triggered from the provider side. The first signal posted to a production webhook executes on the subscriber's real broker accounts. A live URL must never be used to debug request formatting.

Two safe ways to test:

  • Request a demo webhook URL from trade2sync. It behaves exactly like a production endpoint — same authentication, same responses, same validation — but is not connected to any funded account. This is the right way to build and debug an integration.
  • Have the subscriber connect a demo account. Signals then run end to end against a demo broker account, showing real behaviour with nothing at stake.
Use a fresh message_id for every test

Duplicate protection is permanent. Reusing an id from an earlier test returns a clean 200 status code and then does nothing — which looks exactly like a broken integration. If a test seems to vanish, the id should be changed before anything else is debugged.

14. One URL per channel

A provider publishing several signal streams — a gold channel and an indices channel, or a free tier and a premium tier — needs a separate webhook URL from the subscriber for each one.

Reusing a single URL across two streams fails in two ways, and neither reports an error:

  • Signals disappear. Two streams with independent numbering will eventually reuse the same message_id. The second is filtered as a duplicate and answered with a 200 status code.
  • Trades execute under the wrong settings. Every URL carries one channel's identity, so the second stream inherits the first stream's lot sizing, filters and broker routing. That is a wrong trade, not a missing one — the more expensive of the two failures.
Additional webhooks are cheap

Creating another webhook takes the subscriber about ten seconds. A second URL should always be requested rather than reusing one.

15. Code examples

One example per authentication mode. The subscriber's choice at creation time decides which applies — use the one matching the credential that was supplied.

Nothing to send beyond the body.

python
import requests

URL = "https://webhook.trade2sync.com/wh/WEBHOOK_TOKEN"

response = requests.post(URL, json={
    "message_id": 1001,
    "message": "BUY XAUUSD @ 2350 SL 2345 TP 2360",
})
print(response.status_code, response.text)

No encoding step is required — requests builds the header, and the plain form is accepted by platforms that cannot.

python
import requests

URL  = "https://webhook.trade2sync.com/wh/WEBHOOK_TOKEN"
USER = "whk_1a2b3c4d"
PASS = "webhook_password"

response = requests.post(URL, auth=(USER, PASS), json={
    "message_id": 1001,
    "message": "BUY XAUUSD @ 2350 SL 2345 TP 2360",
})
print(response.status_code, response.text)

One fixed header, one issued value, nothing computed per request.

python
import requests

URL = "https://webhook.trade2sync.com/wh/WEBHOOK_TOKEN"
KEY = "THE_ISSUED_KEY_VALUE"

response = requests.post(URL, headers={"X-Webhook-Key": KEY}, json={
    "message_id": 1001,
    "message": "BUY XAUUSD @ 2350 SL 2345 TP 2360",
})
print(response.status_code, response.text)

The only mode that proves the body was not altered in transit. The signature is computed per request, so it requires code.

python
import hmac, hashlib, json, requests

URL    = "https://webhook.trade2sync.com/wh/WEBHOOK_TOKEN"
SECRET = "webhook_secret"

payload = {
    "message_id": 1001,
    "message": "BUY XAUUSD @ 2350 SL 2345 TP 2360",
}

# Serialise ONCE. Sign these bytes, send these bytes.
body = json.dumps(payload, separators=(",", ":")).encode()
signature = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()

response = requests.post(URL, data=body, headers={
    "Content-Type": "application/json",
    "X-Signature": signature,
})
print(response.status_code, response.text)
Serialise once, then sign and send the same bytes

Signing an object and letting the HTTP library re-serialise it produces different bytes and a guaranteed 401 status code. This is the most common signature failure, and it looks like a credential problem when it is not.

16. Go-live checklist

  • The URL is stored as a secret, not in source control
  • date, when sent, is unix seconds generated at send time
  • edit_count stays identical across network retries
  • With signatures: the exact bytes sent are the bytes signed, serialised once
  • Only status codes 429 and 503 are retried, with backoff, honouring Retry-After
  • Status codes 401 and 404 are treated as configuration problems, not transient ones
  • Each signal stream posts to its own URL
  • Testing was done against a demo endpoint, not a live one
  • A 200 status code is understood to confirm delivery, not execution

16.1 Getting help

For anything about a specific webhook — a 404 status code, a switched-off endpoint, a signal that did not fire — contact the subscriber who issued the URL. The subscriber can see the dashboard for that webhook and resolve most issues in seconds.

For a demo URL, or where the API itself appears to be misbehaving, contact trade2sync support.