# Outbound webhooks: account events, pushed to you

> Index of every docs page as Markdown: [llms.txt](https://fxsocket.com/docs/llms.txt). All pages in one file: [llms-full.txt](https://fxsocket.com/llms-full.txt).

Source: https://fxsocket.com/docs/webhooks-out

Register an HTTPS endpoint and FxSocket POSTs a signed JSON event to it within a second or two of something happening on your MetaTrader 4 or MetaTrader 5 account: a position opened or closed, a stop moved, a pending order placed or filled, the balance changed by a trade, the terminal lost its broker. No WebSocket client to keep alive, so it fits serverless functions, n8n, Zapier and Make flows and plain backends alike.

Outbound webhooks are on every plan, at no extra cost. Not to be confused with _inbound_ webhooks, which turn TradingView alerts into orders.

[Open Outbound webhooks](https://app.fxsocket.com/dashboard/integrations/webhooks-out)

## Set it up

### 1. Open Outbound webhooks

In the dashboard go to Integrations and open Outbound webhooks.

### 2. Add your endpoint

Paste the HTTPS URL that should receive the events. Pick the events you want (or all of them) and the accounts they come from (or all of them, including accounts you link later).

### 3. Copy the signing secret

FxSocket shows the endpoint's secret (whsec_…) right after you add it. Store it with your receiver; it is what proves a request came from FxSocket. You can reveal it again or rotate it at any time.

### 4. Send a test event

Press Send test. A webhook.test event arrives within seconds, and the delivery log shows your endpoint's answer. Real events start as soon as something happens on a covered account.

## Events

- `position.opened`: A new position was opened (the first deal of a position).
- `position.increased`: Netting accounts: a deal added volume to an existing position.
- `position.partially_closed`: Part of a position was closed; data.positionVolume is what is left.
- `position.closed`: A position was closed in full.
- `position.reversed`: Netting accounts: one deal closed the position and opened the opposite one.
- `position.modified`: Stop loss or take profit of an open position moved.
- `order.placed`: A pending order was placed.
- `order.modified`: A pending order's price, stops or expiry changed.
- `order.triggered`: A pending order was filled and became a position.
- `order.deleted`: A pending order was deleted.
- `order.expired`: A pending order expired.
- `balance.changed`: A trade moved the balance (profit, commission, swap). Sent in addition to the position event of the same deal. Deposits and withdrawals are not events.
- `terminal.disconnected`: The terminal lost its broker connection, or could not be reached, for three minutes in a row.
- `terminal.connected`: The connection is back, after a terminal.disconnected.
- `webhook.test`: Sent only when you press Send test.

## Payloads

Every request body is one envelope: the event id and type, when it was created, the account it happened on, and the event's `data`.

position.closed:

```json
{
  "id": "evt_01jc8x4m2q9t7v3b5n6r0w2y4z",
  "type": "position.closed",
  "version": 1,
  "createdAt": "2026-11-04T10:15:02.412Z",
  "account": {
    "id": "7f3a1c2e-0b4d-4e5f-9a6b-1c2d3e4fa8b9",
    "platform": "mt5",
    "login": 5012345,
    "server": "TegasFX-Demo",
    "name": "Main demo"
  },
  "data": {
    "deal": 123456789, "order": 123456700, "position": 123456600,
    "symbol": "EURUSD", "type": "Sell", "entry": "Out",
    "positionType": "Buy",
    "volume": 0.10, "price": 1.08421,
    "profit": 12.40, "commission": -0.70, "swap": 0.0,
    "magic": 0, "comment": "",
    "sl": 0.0, "tp": 0.0, "positionVolume": 0.0,
    "remainderTicket": 0, "remainderOf": 0,
    "time": "2026-11-04T12:15:02.301Z",
    "timeUtc": "2026-11-04T10:15:02.301Z",
    "degraded": false
  }
}
```

balance.changed:

```json
{
  "id": "evt_…",
  "type": "balance.changed",
  "version": 1,
  "createdAt": "2026-11-04T10:15:02.700Z",
  "account": { "id": "…", "platform": "mt5", "login": 5012345, "server": "TegasFX-Demo", "name": "Main demo" },
  "data": {
    "delta": 11.70,
    "currency": "USD",
    "balance": 10011.70,
    "equity": 10011.70,
    "balanceAt": "2026-11-04T10:15:02.655Z",
    "deal": { "deal": 123456789, "position": 123456600, "type": "Sell", "entry": "Out",
              "profit": 12.40, "commission": -0.70, "swap": 0.0, "time": "…", "timeUtc": "…" }
  }
}
```

terminal.disconnected:

```json
{
  "id": "evt_…",
  "type": "terminal.disconnected",
  "version": 1,
  "createdAt": "2026-11-04T10:31:00.000Z",
  "account": { "id": "…", "platform": "mt5", "login": 5012345, "server": "TegasFX-Demo", "name": "Main demo" },
  "data": {
    "reason": "broker",
    "since": "2026-11-04T10:28:00.000Z",
    "tradeAllowed": false
  }
}
```

`reason` is `broker` (the terminal runs but has no connection to the trade server) or `unreachable` (FxSocket cannot reach the terminal at all). `tradeAllowed` is the terminal's Algo Trading switch.

- **data is the terminal's own frame.** position.* and order.* carry the fields of the FxSocket WebSocket stream unchanged, plus positionType, the direction of the position (on MT5 a closing deal of a Buy is a Sell deal; positionType says Buy either way). New fields can appear at any time; ignore what you do not know. version only changes on a breaking change.
- **Times.** time is broker server time, timeUtc and createdAt are UTC.
- **MT4.** MT4 has no deal tickets: deal is 0 and order is the order ticket. A partial close shows the remainder's new ticket in remainderTicket.
- **Balance.** delta is profit + commission + swap of the deal. balance and equity are read right after it and may already include a deposit; they are null if that read failed.
- **Reconnects.** If the connection to a terminal drops, FxSocket replays the deals it missed from the terminal's history, so no open, close or balance event is lost. SL/TP changes and pending-order events that happened during the gap are not replayed.

## Headers and signatures

Requests follow [Standard Webhooks](https://www.standardwebhooks.com/), so any of its libraries can verify them. Each one carries:

- `webhook-id`: The event id. Identical on every retry: use it to drop repeats.
- `webhook-timestamp`: Unix seconds of this attempt. Reject requests older than five minutes.
- `webhook-signature`: v1,<base64 HMAC-SHA256>. During a secret rotation two signatures, separated by a space.
- `content-type`: application/json
- `user-agent`: FxSocket-Webhooks/1.0

The signature is an HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{body}`, keyed with the base64-decoded part of your secret after `whsec_`. Check it against the raw body, before any JSON parsing:

Node.js:

```js
import crypto from "node:crypto";

// body: the raw request body as a string, exactly as received.
export function verify(secret, headers, body) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"] ?? "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${ts}.${body}`)
    .digest();

  return signatures.split(" ").some((entry) => {
    const [version, value] = entry.split(",");
    if (version !== "v1" || !value) return false;
    const given = Buffer.from(value, "base64");
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
}
```

Python:

```python
import base64, hashlib, hmac, time

# body: the raw request body as bytes, exactly as received.
def verify(secret: str, headers, body: bytes) -> bool:
    msg_id = headers["webhook-id"]
    ts = headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{ts}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()

    return any(
        hmac.compare_digest(value, expected)
        for version, _, value in (s.partition(",") for s in headers["webhook-signature"].split(" "))
        if version == "v1"
    )
```

Rotating the secret in the dashboard signs every request with both the new and the old secret for 24 hours, so you can switch your receiver over without dropping an event.

## Delivery and retries

- **Success:** Any 2xx answer within 10 seconds. The body is ignored.
- **Retries:** Anything else (4xx, 5xx, a redirect, a timeout) is retried after 10 s, 1 min, 5 min, 30 min, 2 h, 6 h and 12 h, with some jitter: 8 attempts over about 20 hours.
- **410 Gone:** Turns the endpoint off at once. Answer 410 when you want FxSocket to stop.
- **Failing endpoints:** An endpoint without a single successful delivery for 72 hours is turned off, and you get an email. Turn it back on in the dashboard once it works again.
- **At least once:** An event can arrive more than once (a retry after a timeout your server did not notice). Keep the webhook-id of what you processed and drop repeats.
- **Order:** Not guaranteed: a retried event can arrive after a newer one, and up to four requests per endpoint run at once. Order by data.time and use the ids and positionVolume, not the arrival order.
- **Redirects:** Not followed. Use the final URL.

The delivery log in the dashboard shows the last 14 days: every event, its payload, your endpoint's last answer, and a Redeliver button.

## Limits

- Up to 5 endpoints per user, on every plan.
- The delivery log keeps 14 days.
- Endpoints must use HTTPS on port 443 at a public address with an IPv4 record; private, loopback and cloud-internal addresses are refused.
- Privately hosted accounts are not supported yet.

## What leaves FxSocket

Webhooks send trading data of the accounts you chose (positions, orders, balance and equity, the account's login and server) to the URL you entered. FxSocket reads it from your terminal read-only: the connection behind webhooks can never place, change or close an order. More in the [privacy policy](https://fxsocket.com/privacy#outbound-webhooks).

## Troubleshooting

- **The test arrives, real events do not:** Check Accounts on the Outbound webhooks page. An account shows Streaming once FxSocket is connected to its terminal, which takes a minute or two after you add an endpoint.
- **Signature does not match:** Verify against the raw body exactly as received, before any JSON parsing, and with the secret of this endpoint. After a rotation the old secret is still signed for 24 hours.
- **Events may be missing:** The connection to the terminal was down for longer than the terminal's history replay covers (24 hours). The page names the window; the account's history in the API has the full picture.

## Support

Email [info@fxsocket.com](mailto:info@fxsocket.com) with the endpoint URL and the event id from the delivery log. We answer every message.

## Related

- [MT5 REST and WebSocket API](https://fxsocket.com/docs/mt5.md)
- [Notion trade journal](https://fxsocket.com/docs/notion.md)
- [Account API (v1)](https://fxsocket.com/docs/api.md)
- [MT4 and MT5 trade alerts to Discord, Telegram or Slack](https://fxsocket.com/blog/mt4-mt5-trade-alerts-webhook)
