# TradingView alerts to MT5 — no EA, no VPS

URL: https://docs.fxapis.com/guides/tradingview

> Turn TradingView alerts into MetaTrader 5 orders with a secret webhook URL: create a hook in the console or the API, set up the TradingView alert step by…



TradingView can send an alert to a URL, but it cannot add an `Authorization`
header — so it cannot call a normal API. The usual answer is an Expert Advisor
on a Windows VPS, or a paid bridge. fxapis gives you a **secret alert URL**
instead: paste it into a TradingView alert, and the alerts that fire become
orders on your MT5 account. We run the terminal; nothing of yours has to stay
switched on.

```
TradingView alert fires
   → POST https://api.fxapis.com/hooks/tradingview/<secret>     answered at once (202)
   → fxapis places the order on your MT5 account(s)             the same path as POST /v1/…/orders/market
   → the alert and its result appear under TradingView in the console
```

<Callout type="warn" title="Test with a demo account">
  An alert hook places real orders on whatever account it points at. Point your first hook at a
  **broker demo account** and fire a test alert before using a real-money account.
</Callout>

## What you need [#what-you-need]

* An fxapis workspace with an MT5 account connected ([Connect MT5 accounts](/accounts)).
  For alerts that must act within a second, set the account to **always connected** — an account
  that comes online on demand has to connect first, which takes about 10 seconds in our measurement.
* A TradingView plan that includes **webhook alerts** — TradingView's
  [pricing page](https://www.tradingview.com/pricing/) lists webhook notifications on its paid
  plans, not on Basic — and **two-factor authentication** switched on in your TradingView account:
  TradingView sends webhooks only from accounts with 2FA
  ([About webhooks](https://www.tradingview.com/support/solutions/43000529348-about-webhooks/)).
  Both checked 30 September 2026.

## 1. Create a hook [#1-create-a-hook]

### In the console [#in-the-console]

Open **Console → TradingView → Create a TradingView hook**, then:

1. **Name** it after the strategy or chart it serves.
2. Choose the **account**. With several accounts, each alert places one
   [multi-account order](/guides/multi-account-orders) — this needs a plan with multi-account orders.
3. Set the **default volume** in lots. Alerts that do not send a volume use it.
4. Tell it how your broker names symbols: a **symbol map** (`XAUUSD=XAUUSD.a`, one per line) and/or a
   **suffix** added to every other symbol (`.a`, `m`, `.pro`…).
5. Leave **Only from TradingView** on unless you are sending test alerts yourself.

The console then shows the **URL, once**, with ready-to-paste messages. Copy it now: we store only a
hash, so it cannot be shown again. If you lose it, **rotate** the hook to get a new one.

Creating and changing hooks needs a workspace **owner or admin** — a hook's URL can open positions,
so it takes the same standing as creating a trading key.

### With the API [#with-the-api]

With a key holding `trading:execute`:

```bash
curl -sS -X POST "https://api.fxapis.com/v1/alert-hooks" \
  -H "Authorization: Bearer $FXAPIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Gold breakout",
    "accountIds": ["2b0c7c1e-5d0b-4d8e-9a53-0e6f3b1c9a10"],
    "defaults": {
      "volume": "0.10",
      "deviationPoints": 30,
      "symbolMap": { "XAUUSD": "XAUUSD.a" },
      "symbolSuffix": "",
      "stripExchangePrefix": true,
      "allowClose": true,
      "onlyTradingViewIps": true
    }
  }'
```

```json
{
  "data": {
    "id": "8f1d…",
    "name": "Gold breakout",
    "url": "https://api.fxapis.com/hooks/tradingview/3q2-7ZkT0d8m1sQe9yVbW4nX6pLr5aHcUgJf0oKxQx4w",
    "urlHint": "https://api.fxapis.com/hooks/tradingview/…Qx4w",
    "enabled": true,
    "…": "…"
  }
}
```

`url` appears in this response and in `POST /v1/alert-hooks/{id}/rotate`, and nowhere else. Every
other endpoint shows `urlHint`.

| Endpoint                              | What it does                                      | Scope             |
| ------------------------------------- | ------------------------------------------------- | ----------------- |
| `GET /v1/alert-hooks`                 | List hooks                                        | `trading:read`    |
| `POST /v1/alert-hooks`                | Create one; returns `url` once                    | `trading:execute` |
| `GET /v1/alert-hooks/{id}`            | Read one                                          | `trading:read`    |
| `PATCH /v1/alert-hooks/{id}`          | Change name, accounts or defaults — the URL stays | `trading:execute` |
| `POST /v1/alert-hooks/{id}/rotate`    | New URL, returned once; the old one stops at once | `trading:execute` |
| `POST /v1/alert-hooks/{id}/disable`   | Pause: alerts are refused and logged              | `trading:reduce`  |
| `POST /v1/alert-hooks/{id}/enable`    | Resume                                            | `trading:execute` |
| `DELETE /v1/alert-hooks/{id}`         | Delete; the URL stops working for good            | `trading:execute` |
| `GET /v1/alert-hooks/{id}/deliveries` | Every alert received and what became of it        | `trading:read`    |

## 2. Set up the alert in TradingView [#2-set-up-the-alert-in-tradingview]

<Steps>
  <Step>
    ### Open the alert dialog [#open-the-alert-dialog]

    On the chart, open **Create alert** (the alarm-clock icon, or <kbd>Alt</kbd>+<kbd>A</kbd>). For a
    strategy, choose the strategy as the **Condition**; for an indicator or a drawing, choose it and the
    crossing or level you want.
  </Step>

  <Step>
    ### Write the message [#write-the-message]

    In **Message**, paste one of the messages below. TradingView replaces the `{{…}}` placeholders when
    the alert fires.
  </Step>

  <Step>
    ### Paste the URL [#paste-the-url]

    Under **Notifications**, tick **Webhook URL** and paste the hook's URL.
  </Step>

  <Step>
    ### Create it and check the log [#create-it-and-check-the-log]

    Save the alert. When it fires, it appears under **Console → TradingView → your hook** within a
    second, with its result. TradingView's own alert log has a webhook status column too.
  </Step>
</Steps>

## 3. Messages [#3-messages]

A message can be JSON or a line of text; TradingView sends JSON as `application/json` and anything
else as `text/plain`, and fxapis reads both.

### Indicators and drawings [#indicators-and-drawings]

An indicator alert knows nothing about positions, so it sends a fixed action. Make one alert per
direction:

```json title="Buy"
{ "action": "buy", "symbol": "{{ticker}}", "id": "{{ticker}}-{{timenow}}" }
```

```json title="Sell"
{ "action": "sell", "symbol": "{{ticker}}", "id": "{{ticker}}-{{timenow}}" }
```

Add levels or a volume if the alert knows them:

```json
{ "action": "buy", "symbol": "{{ticker}}", "volume": "0.20", "sl": "2310.5", "tp": "2385", "id": "{{ticker}}-{{timenow}}" }
```

### Strategies [#strategies]

One alert covers every entry, exit and reversal of a strategy:

```json title="Strategy"
{
  "action": "{{strategy.order.action}}",
  "symbol": "{{ticker}}",
  "position": "{{strategy.market_position}}",
  "prev_position": "{{strategy.prev_market_position}}",
  "id": "{{strategy.order.id}}-{{timenow}}"
}
```

`{{strategy.order.action}}` alone cannot tell an exit from an entry — closing a long is a `sell`. So
when `position` is present, the position **after** the order decides:

| `position` after the order | `prev_position`         | What fxapis does                                 |
| -------------------------- | ----------------------- | ------------------------------------------------ |
| `flat`                     | anything                | Closes every open position on the symbol         |
| `short`                    | `long` (a reversal)     | Closes the symbol's positions, then sells        |
| `long`                     | `short` (a reversal)    | Closes the symbol's positions, then buys         |
| same side as the order     | `flat` or the same side | Opens (an entry, or adding to one)               |
| opposite side to the order | the same                | **Ignored** — a partial exit. Logged, not traded |

Partial exits are not traded because there is no single honest MT5 translation: on a hedging account
a "sell 1 of 3" could close part of a position or open a hedge, and guessing wrong is worse than
doing nothing. Exit fully, or let stops take partial profits.

<Callout type="warn" title="Volume is lots, not contracts">
  TradingView sizes forex strategies in **units**: `{{strategy.order.contracts}}` is `100000` for what
  MT5 calls one lot. The template above sends no volume, so the hook's default lots are used. Send
  `volume` only if it is in lots. Anything above 1000 lots is refused as a units mistake.
</Callout>

### Plain text [#plain-text]

Action, symbol, optional volume, then optional `key=value` pairs:

```text
buy {{ticker}} 0.10
sell {{ticker}} 0.10 sl=1.1050 tp=1.0900 id={{timenow}}
close {{ticker}}
close_all
```

### Fields [#fields]

| Field           | Also accepted as            | Meaning                                                                                              |
| --------------- | --------------------------- | ---------------------------------------------------------------------------------------------------- |
| `action`        | `side`, `signal`            | `buy` (`long`), `sell` (`short`), `close` (`exit`), `close_long`, `close_short`, `flat`, `close_all` |
| `symbol`        | `ticker`                    | Usually `{{ticker}}`. Mapped as described below. Not needed for `close_all`                          |
| `volume`        | `qty`, `lots`, `contracts`  | Lots, e.g. `"0.10"`. Default: the hook's volume                                                      |
| `sl`            | `stopLoss`, `stop_loss`     | Absolute price. `0` means none                                                                       |
| `tp`            | `takeProfit`, `take_profit` | Absolute price. `0` means none                                                                       |
| `id`            | `alert_id`                  | Unique per alert — see [duplicates](#duplicates)                                                     |
| `position`      | `market_position`           | `{{strategy.market_position}}`                                                                       |
| `prev_position` | `prev_market_position`      | `{{strategy.prev_market_position}}`                                                                  |
| `comment`       |                             | Written on the MT5 order (first 31 characters). Default `TradingView`                                |

Numbers may be sent as JSON numbers or strings. Placeholders you might use: `{{ticker}}`,
`{{exchange}}`, `{{close}}`, `{{time}}`, `{{timenow}}`, `{{interval}}`, and in strategies
`{{strategy.order.action}}`, `{{strategy.order.contracts}}`, `{{strategy.order.price}}`,
`{{strategy.order.id}}`, `{{strategy.order.comment}}`, `{{strategy.market_position}}`,
`{{strategy.prev_market_position}}`, `{{strategy.position_size}}`.

<Callout title="Quote your placeholders">
  `"volume": {{strategy.order.contracts}}` is valid JSON only if the placeholder fills in. Quoted
  (`"{{…}}"`) it always is. A message that is almost JSON is refused with a reason saying so.
</Callout>

## Symbols [#symbols]

In order:

1. **Exchange prefix removed** — `OANDA:XAUUSD` becomes `XAUUSD` (unless you turn this off).
2. **Symbol map** — if the map names the symbol, its value is used exactly: `XAUUSD` → `XAUUSD.a`.
3. **Suffix** — otherwise the suffix is added: with `.a`, `EURUSD` → `EURUSD.a`.

The symbol the alert resolved to is shown on every delivery, so a wrong mapping is easy to spot.

## Closing [#closing]

| Action                                     | Closes                                                                                |
| ------------------------------------------ | ------------------------------------------------------------------------------------- |
| `close`, `flat` (or a strategy going flat) | Every open position **on the symbol**, buys and sells, on each of the hook's accounts |
| `close_long` / `close_short`               | Only the symbol's buys / only its sells                                               |
| `close_all`                                | **Every** open position on the hook's accounts, every symbol                          |

Closing acts on the positions the account holds, not only on those an alert opened: MT5 does not
record who opened a position in a way that can be trusted, and "close XAUUSD" meaning "close the
XAUUSD I can see" is what traders expect. Positions are read fresh from the terminal first. Each
position closed is its own order in your history. With nothing open, the alert is logged as
`ignored`.

Turn **Act on close alerts** off (`"allowClose": false`) if you exit with stops only — close actions
are then logged as `ignored`, and reversals are ignored too.

Closing is never refused for your plan's limits: you can always get flat.

## Duplicates [#duplicates]

TradingView fills `{{timenow}}` once, when the alert fires, so a resend of one alert carries the same
`id` and a new alert a new one. fxapis sends each `id` **once**: a repeat is answered `200` with
`"status": "duplicate"`, logged, and not traded — even if the copies arrive at the same moment.

Without an `id`, the same message twice within **60 seconds** is treated as one alert. That also
means two genuinely separate identical alerts within a minute are traded once — another reason to
include an `id`.

## Security [#security]

* **The URL is the credential.** 32 random bytes, shown once, stored only as a SHA-256 hash. Anyone
  holding the URL can trade the hook's accounts, so keep it out of screenshots and shared
  templates. **Rotate** if it leaks: the old URL stops working at once.
* **Only from TradingView** (on by default) refuses any request not sent from TradingView's
  published webhook addresses: `52.89.214.238`, `34.212.75.30`, `54.218.53.128`, `52.32.178.7`.
  Turn it off only to send test alerts from your own machine.
* **Pause** a hook to refuse its alerts without touching TradingView. **Delete** it to end the URL
  for good; its orders stay in your history.
* Alerts go through exactly the checks an API order does: the account must be able to trade, your
  plan must allow it, and nothing is sent while trading is switched off for your workspace.
* Don't put secrets in alert messages. The message is stored (up to 2 KB) with each delivery so you
  can see what arrived; if you paste the hook URL into a message, it is removed before storing.

## Troubleshooting [#troubleshooting]

Alerts that reach a hook are listed under **Console → TradingView → your hook** (or
`GET /v1/alert-hooks/{id}/deliveries`) for 30 days, with a status and a reason in plain words.
Past 30 alerts a minute, only one row a minute is recorded:

| Status      | Meaning                                                                                                                                      |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`   | Received; the order is being sent                                                                                                            |
| `accepted`  | At least one order reached the broker. Each order's own result — filled, or rejected by the broker with its reason — is in the order history |
| `rejected`  | Nothing was sent. The reason says why                                                                                                        |
| `duplicate` | A repeat of an alert already taken                                                                                                           |
| `ignored`   | Understood and deliberately not traded: a partial exit, nothing to close, closing switched off, an unknown action                            |

What TradingView sees:

| HTTP                       | Meaning                                           |
| -------------------------- | ------------------------------------------------- |
| `202`                      | Taken; the order is being sent                    |
| `200`                      | Duplicate or ignored — nothing more will happen   |
| `400` `INVALID_ALERT`      | The message could not be read — see the reason    |
| `403` `SENDER_NOT_ALLOWED` | Not from TradingView's addresses                  |
| `404`                      | No such hook — a mistyped, rotated or deleted URL |
| `409` `HOOK_DISABLED`      | The hook is paused                                |
| `429` `RATE_LIMITED`       | More than 30 alerts in a minute                   |

Common causes:

* **Nothing in the list at all** — the alert did not reach us. Check the URL was pasted whole, that
  **Webhook URL** is ticked, and TradingView's alert log. A rotated or deleted URL answers `404`.
* **`rejected` — not from TradingView's addresses** — you sent a test with curl or another tool. Turn
  off **Only from TradingView** while testing.
* **`rejected` — the account is offline / not ready** — the account was not connected. Keep it always
  connected, or check it under **Accounts**.
* **The broker rejected an unknown symbol** — the resolved symbol is shown on the delivery; add a
  symbol map entry or a suffix.
* **`rejected` — plan limits** — the month's orders are used up, or several accounts on a plan
  without multi-account orders.

## Limits [#limits]

* **30 alerts a minute per hook.** Past that, alerts are refused with `429`, and one row records it.
* **25 hooks per workspace.**
* **Market orders only** from alerts, with optional stop loss and take profit. Pending orders go
  through the [API](/trading).
* **One account per hook on every plan**; several accounts on plans with multi-account orders — up to
  the plan's accounts-per-order limit.
* Alerts are **orders**: they count towards the plan's monthly orders.
* Deliveries are kept **30 days**. Orders stay in the order history.
* fxapis answers TradingView immediately — TradingView cancels a webhook request that takes longer
  than three seconds ([About webhooks](https://www.tradingview.com/support/solutions/43000529348-about-webhooks/),
  checked 30 September 2026) — and sends the order just after, in the order alerts arrived for that hook.
