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.
| Term | Meaning in this guide |
|---|---|
| Provider | The party sending signals — a signal service, trading platform, bot or alerting tool. This guide is written for providers. |
| Subscriber | The trade2sync user who created the webhook and whose broker accounts execute the signals. |
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.
-
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 -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 -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):
{"status": "accepted"} -
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.
-
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 subscriber | Required action |
|---|---|
| A URL and nothing else | Post to it. No headers needed. See No authentication |
| A URL, a username and a password | Send an Authorization header. See Basic |
| A URL and a key value | Send it in a header. See Custom header |
| A URL and a secret | Sign each request. See Signature |
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.
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:
# 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.
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.
| Detail | Value |
|---|---|
| Header | X-Signature |
| Algorithm | HMAC-SHA256 |
| Encoding | Lowercase hex, no prefix — not sha256=, not base64, not uppercase |
| Signed over | The exact bytes of the request body |
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.
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.
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:
| Field | Type | Description | |
|---|---|---|---|
| message | string | required | The signal text, exactly as published to a human audience. May be "" only when an image is attached |
| message_id | integer | essential | The 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 |
| date | integer | optional | When 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_id | integer | optional | The message_id this message updates. See Managing a trade |
| edit_count | integer | optional | Defaults to 0. Incremented to correct a signal that was already sent |
| media | object | optional | A base64 image. See Images |
| channel_id | optional | optional | Accepted and stored if any; but system will use the webhook id as channel id, because the URL already identifies the channel |
| channel_name | optional | optional | Accepted and stored if any; but system will use the webhook name as channel name, because the URL already identifies the channel |
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.
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.
{
"message_id": 1002,
"message": "BUY XAUUSD @ 2350 SL 2345 TP 2360",
"media": {
"type": "photo",
"data": "iVBORw0KGgoAAAANSUhEUgAA..."
}
}
| Rule | Limit |
|---|---|
| Formats | PNG or JPEG |
| Decoded image size | 5 MB maximum |
| Whole request | 7 MB maximum — base64 adds roughly a third |
| Encoding | Standard base64. No data:image/png;base64, prefix |
| media.type | Use "photo" |
An image-only signal is allowed: set message to "" and attach the media.
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.
| Sequence | message_id | edit_count | Result |
|---|---|---|---|
| Original | 1001 | 0 | Processed |
| Same message sent twice | 1001 | 0 | Ignored as a duplicate — safe |
| Correction | 1001 | 1 | Processed as an update |
| Second correction | 1001 | 2 | Processed as an update |
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.
{
"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
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.
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.
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.
Received and authenticated. Includes duplicates — a repeat is accepted and then filtered.
{"status": "accepted"}
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."}
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."}
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."}
The signal could not be queued. The request itself was fine — retry it with backoff.
{"detail": "Temporarily unavailable."}
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."}
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.
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 wrong | The fix |
|---|---|
| Completely empty body | Send the signal text, or a JSON object containing message |
| JSON object with no message key | Add message, or send the text as a plain body instead |
| message_id sent as a string | Send integers — 1001, not "1001" |
| Negative or non-integer edit_count | Zero or a non-negative number |
| Image over 5 MB or unreadable | Compress 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 direction | Make 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.
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
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.
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.
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.
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.
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.
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.
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)
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.