Docs

Webhooks

Get a signed HTTPS POST when an order fills, a position opens or closes, an account changes state, or a multi-account order finishes.

Instead of polling, give fxapis a URL and it tells you when something happens. Each event is a signed POST, sent within a few seconds and retried for about two days if your server is down.

Webhooks are on every paid plan. Manage them in the console under Webhooks, or with the API (/v1/webhooks, scope webhooks:write).

Add an endpoint

curl https://api.fxapis.com/v1/webhooks \
  -H "Authorization: Bearer $FXAPIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/fxapis/webhooks", "events": ["order.filled", "position.closed"]}'

The response carries secret (whsec_…). It is shown once — store it with your other secrets. Leave out events to receive every event. Up to 10 endpoints per workspace; each URL must be HTTPS on port 443 and reachable from the internet.

Send yourself a test event to check the wiring:

curl -X POST https://api.fxapis.com/v1/webhooks/$ENDPOINT_ID/test \
  -H "Authorization: Bearer $FXAPIS_API_KEY"

What arrives

POST /fxapis/webhooks HTTP/1.1
Content-Type: application/json
fxapis-event-id: 5b0e7c1a-2f7d-4c1e-9a53-0e6f3b1c9a10
fxapis-signature: t=1791400000,v1=5f2b…

{
  "id": "5b0e7c1a-2f7d-4c1e-9a53-0e6f3b1c9a10",
  "type": "order.filled",
  "createdAt": "2026-10-08T12:00:00.000Z",
  "data": { "id": "…", "accountId": "…", "symbol": "EURUSD", "state": "filled", "previousState": "sending", "…": "…" }
}

data is the resource exactly as the REST API returns it — the order from GET /v1/orders/{id}, the position from the positions list, the account from GET /v1/accounts/{id}, the multi-account order from GET /v1/execution-waves/{id} — read when the event is sent, normally within two seconds of the change.

EventWhendata
order.filledAn order filled, fully or partly — from the broker's reply or found later by reconciliationThe order, with previousState
order.rejectedThe broker refused an orderThe order
order.unresolvedAn order was sent and no answer came back; reconciliation takes overThe order
order.resolvedAn unresolved order's outcome was found (an order.filled follows if it filled)The order
order.cancelledA pending order was withdrawnThe order
position.openedA position appeared on the accountThe position
position.closedA position is gone from the account, whatever closed itThe position
account.state_changedThe account became ready, offline or standby, or needs you (invalid_credentials, needs_2fa, needs_certificate, trading_disabled, error)The account, with previousState
wave.settledA multi-account order finished: settled, cancelled or abandonedThe multi-account order with its legs
webhook.testYou asked for a test{ endpointId, message }

Positions come from the account's position sync, which runs about every 15 seconds while the account is online — so position.opened and position.closed arrive up to about 15 seconds after the broker changed them. A position that was already open when the account connected is reported as opened the first time it is seen.

Verify the signature

Check every delivery before trusting it. fxapis-signature is t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" with your secret>. Use the raw body — the bytes as they arrived, before JSON parsing — and reject anything more than 5 minutes old.

Node / TypeScript (npm install fxapis):

import { verifyWebhook } from "fxapis";

app.post("/fxapis/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = await verifyWebhook(req.body.toString("utf8"), req.header("fxapis-signature"), process.env.FXAPIS_WEBHOOK_SECRET!);
  } catch {
    return res.sendStatus(400);
  }
  if (await alreadyHandled(event.id)) return res.sendStatus(200);
  await handle(event);
  res.sendStatus(200);
});

Python (pip install fxapis):

from fxapis import WebhookVerificationError, webhooks

@app.post("/fxapis/webhooks")
def receive(request):
    try:
        event = webhooks.verify(request.body, request.headers.get("fxapis-signature"), FXAPIS_WEBHOOK_SECRET)
    except WebhookVerificationError:
        return Response(status=400)
    if already_handled(event["id"]):
        return Response(status=200)
    handle(event)
    return Response(status=200)

Any language: split the header on ,; take t and every v1; compute HMAC-SHA256(secret, t + "." + rawBody) as lowercase hex; accept if it equals any v1 (compare in constant time) and t is within 300 seconds of now.

Answer quickly, and expect repeats

  • Answer 2xx within 10 seconds. Do slow work after answering. Redirects are not followed — a 3xx counts as a failure.
  • Delivery is at least once: the same event can arrive twice (a retry after a timeout, or a redelivery you asked for). Deduplicate on id.
  • Events for different resources can arrive out of order. Use createdAt and the resource's own state, not arrival order.

Retries and switching off

A delivery that does not get a 2xx is retried after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours — eight attempts over about 45 hours — then given up. Each endpoint retries on its own: one failing endpoint never delays another.

After 20 failed deliveries in a row the endpoint is switched off and the workspace owner gets an email. Fix it and resume it in the console (or POST /v1/webhooks/{id}/enable). Events already queued to it before it was switched off can be redelivered from its delivery log (POST /v1/webhooks/{id}/deliveries/{eventId}/redeliver); events recorded while it was off never reached that log and can't be redelivered that way. Events and delivery logs are kept for 30 days.

Rotate the secret

POST /v1/webhooks/{id}/rotate returns a new secret, once. For the next 24 hours every delivery is signed with both secrets — two v1= values — so you can deploy the new secret and then retire the old one without missing an event.

On this page