> ## Documentation Index
> Fetch the complete documentation index at: https://docs.innova-trading.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Signal Providers

> Send your strategy signals to Innova Trading over HTTP, without sharing your code

## Overview

If you run your own strategy, you can connect it to Innova's copy engine
**without sharing your code with us**. You run the strategy on your own
server and send us only the signal it already generated. We validate it,
check that it is still fresh, and copy it to the subscribed accounts.

<Note>
  The provider never sends us their code, only signals.
</Note>

All examples use the production host `https://cloud.innova-trading.com`.
Always develop against the **sandbox endpoint** (`/signals/test`) first.

## How to get access

Access is granted by Innova. There is no self-service sign-up yet.

1. Ask your contact at Innova for a **provider token** and the
   **`x-community-slug`** of your community (for example `my-community`).
2. Keep both private. Never paste them in chats, emails or repositories.
3. Start with the sandbox endpoint (`/signals/test`) before sending anything real.

## Endpoints

| Method | URL | What it does |
| - | - | - |
| `POST` | `/api/v1/provider/signals` | **Real.** Validates, filters and publishes the signal to the copy engine. |
| `POST` | `/api/v1/provider/signals/test` | **Sandbox.** Runs the same validations and tells you what would have happened, **without publishing or opening anything**. |
| `GET` | `/api/external/bars?symbol=&timeframe=&limit=` | **Candles** for your strategy (up to 1000 per request; the last one is still forming). Same token and `x-community-slug`. |
| `GET` | `/api/v1/provider/signals/{signal_id}` | Reads back a signal you already sent, using your own `signal_id`. |
| `GET` | `/api/v1/provider/signals/decisions` | Latest decisions about your signals, **including rejections and their reason**. Accepts `?limit=` (1 to 200, default 50). |

## Authentication

Two headers on every call:

```text theme={null}
Authorization: Bearer <YOUR_TOKEN>
x-community-slug: <YOUR_COMMUNITY>
Content-Type: application/json
```

* This is **not** a free-form API key. The token is personal and is handed to
  you by Innova (see "How to get access"). The `x-community-slug` header is
  **mandatory** on every call.
* The token is personal: whoever has it can send signals in
  your name, so do not share it and never paste it in chats or emails.
* No token, or a revoked token: **HTTP 401**.
* Your signals are isolated inside your community.

## The JSON you send

```json theme={null}
{
  "signal_id": "my-strategy-EURUSD-15m-20261011T1430Z",
  "symbol": "EURUSD",
  "direction": "buy",
  "entry": 1.10000,
  "generated_at": "2026-10-11T14:30:05.120Z",
  "sl": 1.09500,
  "tps": [1.10500, 1.11000, 1.11500],
  "lots": 0.10,
  "risk_percent": 1.0,
  "strategy": "my-strategy",
  "comment": "confirmed cross on M15",
  "timeframe": 15
}
```

| Field | Type | Required | Detail |
| - | - | - | - |
| `signal_id` | text (1-120) | **Yes** | **Your** identifier. It is the idempotency key. |
| `symbol` | text (2-20) | **Yes** | E.g. `EURUSD`, `XAUUSD`. Uppercased on our side. |
| `direction` | text | **Yes** | `buy` or `sell` (we also accept `long` / `short`). |
| `entry` | number or `"market"` | **Yes** | Entry price, or `"market"` to use the live price. |
| `generated_at` | ISO-8601 | **Yes** | When **your strategy** generated the signal, not when you sent it. Use UTC with `Z`. |
| `sl` | number | No | Stop loss. |
| `tps` | list of numbers | No | Take profits in order (TP1, TP2, TP3...). |
| `lots` | number > 0 | No | Suggested lot size. Each follower defines the final lot. |
| `risk_percent` | number, 0 to 100 | No | Suggested risk. Same: followers decide. |
| `strategy` | text (max 40) | No | Strategy name. It defines the source accounts subscribe to. |
| `comment` | text (max 500) | No | Free note, returned as you sent it. |
| `timeframe` | integer >= 0 | No | In minutes. |

<Warning>
  **We do not check that `sl` and `tps` are on the right side of `entry`.**
  Today the server does not reject a stop loss on the wrong side (for example
  an `sl` above `entry` on a `buy`). Validate this in your own code before
  sending: a wrongly placed stop is accepted as sent.
</Warning>

<Warning>
  **An unknown field makes the call fail with HTTP 422.** This is on purpose:
  if you write `stop_loss` instead of `sl`, we prefer to tell you rather than
  accept a signal without a stop. A `generated_at` without a time zone is
  read as UTC. Keep your server clock synchronized with NTP.
</Warning>

## Idempotency

The pair **(your provider, your `signal_id`)** is unique on our side.

* If you retry because of a network failure, send **the same `signal_id`**
  (and the same body).
* The second call returns `"status": "duplicate"` with the first result and
  no extra trade is opened.
* Never reuse a `signal_id` for a different signal: the second one would be
  discarded as a repeat.
* We also answer `duplicate` with code `active_same_direction` if you already
  have an open signal on the same symbol and direction. We do not stack
  positions.

A good `signal_id` is deterministic: strategy + symbol + timeframe + candle
time. That way, restarting your process on the same candle never duplicates.

## Freshness

A late signal cannot be copied at a price that no longer exists. Before
publishing, we check:

| Rule | What it looks at | Default |
| - | - | - |
| Age | `now - generated_at` | max **120 s** |
| Clock ahead | `generated_at` in the future | tolerance **30 s** |
| Price drift | how far the market moved **against** your `entry` | max **0.2 %** |
| Absolute price distance | how far your `entry` is from the live price, **in either direction** | max **1 %** |
| Only if improved | accepts late signals only if the price improved for the follower | **off** |

Today these values are **global** (the same for every provider). If your
strategy works on longer timeframes and 120 s is too short, tell us.

The 0.2 % drift rule is directional: a move **in your favor** never rejects
the signal. The 1 % absolute rule is not: an `entry` more than 1 % away from
the live market, on either side, is rejected with `price_drift_exceeded`,
because that price does not exist in the market. If there is no live quote to
compare against, a signal with a numeric `entry` is also rejected
(`no_market_price`, or `price_drift_exceeded` if the price cannot be measured).

## Rejection codes

Branch on `code`, never on the text of `detail`.

| `code` | What happened | What to do |
| - | - | - |
| `stale` | The signal arrived older than the allowed maximum. | Check your process latency. Do not retry with the same `generated_at`. |
| `clock_ahead` | Your `generated_at` is in the future beyond the tolerance. | Your server clock is wrong. Sync with NTP and use UTC. |
| `price_drift_exceeded` | The market moved against you more than the limit, or your `entry` is more than 1 % away from the live price. | Correct to reject: entering there would be a different trade. |
| `no_improvement` | Late signal and the price did not improve (optional rule, if enabled). | Nothing: this is the requested behavior. |
| `missing_generated_at` | `generated_at` is missing or not a valid ISO-8601 date. On this route the API answers with an HTTP `422` validation error first (the field is required in the contract), so this code does not reach the client. | Add it. |
| `no_market_price` | You sent `"entry": "market"` and we had no live price for the symbol. | Retry, or send a numeric price. |
| `bus_unavailable` | We stored the signal but our internal bus was down: **it was not copied**. | Tell us. It is on our side. |
| `duplicate` | We already received that `signal_id`. | Nothing. |
| `active_same_direction` | You already have an open signal in the same direction. | Nothing. |

**Nothing is silently dropped.** Every decision, accepted or rejected, is
recorded and available at `GET /api/v1/provider/signals/decisions`.

## The response

```json theme={null}
{
  "accepted": true,
  "status": "published",
  "code": "ok",
  "detail": "Señal aceptada y publicada al motor de copia.",
  "signal_id": "my-strategy-EURUSD-15m-20261011T1430Z",
  "signal_source": "provider:aaaaaaaa:my-strategy",
  "symbol": "EURUSD",
  "direction": "buy",
  "generated_at": "2026-10-11T14:30:05.120000+00:00",
  "received_at": "2026-10-11T14:30:05.480000+00:00",
  "published": true,
  "dry_run": false,
  "freshness": { "accepted": true, "code": "fresh", "age_seconds": 0.36 }
}
```

(Abridged response: it carries more fields.)

* `status`: `published` (copied), `simulated` (sandbox), `duplicate` or `rejected`.
* `published` is `true` **only** if the signal reached the copy engine. If it
  is `false`, read `code`.
* `signal_source` is the identifier accounts use to subscribe to your
  strategy. Send it to your community owner.
* `received_at - generated_at` is your delivery latency. Log it.

HTTP codes: `200` whenever the call was well formed (even if the signal was
rejected: the reason is in `code`), `401` without a valid token, `409` when the
signal could not be saved because of a rare uniqueness conflict (retry with a different
`signal_id`), `422` for a bad JSON, `429` above 120 signals per minute.

## Sandbox

`POST /api/v1/provider/signals/test` is where you should develop. It does the
same as the real endpoint (validates the JSON, runs the freshness filter,
resolves the symbol against subscribed accounts, computes lot size and
partial-close plan) and returns exactly what would happen, including
rejection reasons. It **never** publishes or opens anything.

The visible difference: `"status": "simulated"`, `"dry_run": true`,
`"published": false`, and an extra `execution_preview` block. If
`targets_matched` is `0`, your signal is valid but no account is subscribed to
your `signal_source` yet (the community owner sets that up).

## Examples

<CodeGroup>
  ```python Python theme={null}
  import requests
  from datetime import datetime, timezone

  BASE = "https://cloud.innova-trading.com"
  headers = {
      "Authorization": "Bearer <YOUR_TOKEN>",
      "x-community-slug": "<YOUR_COMMUNITY>",
  }
  now = datetime.now(timezone.utc)
  signal = {
      "signal_id": "my-strategy-EURUSD-15m-20261011T1430Z",  # same id on every retry
      "symbol": "EURUSD",
      "direction": "buy",
      "entry": 1.10000,
      "generated_at": now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z",
      "sl": 1.09500,
      "tps": [1.10500, 1.11000],
      "strategy": "my-strategy",
      "timeframe": 15,
  }

  # Sandbox first: /signals/test publishes nothing
  resp = requests.post(f"{BASE}/api/v1/provider/signals/test", json=signal,
                       headers=headers, timeout=10)
  body = resp.json()
  print(resp.status_code, body["status"], body["code"])
  ```

  ```bash curl theme={null}
  curl -X POST "https://cloud.innova-trading.com/api/v1/provider/signals/test" \
    -H "Authorization: Bearer <YOUR_TOKEN>" \
    -H "x-community-slug: <YOUR_COMMUNITY>" \
    -H "Content-Type: application/json" \
    -d '{
      "signal_id": "my-strategy-EURUSD-15m-20261011T1430Z",
      "symbol": "EURUSD",
      "direction": "buy",
      "entry": 1.10000,
      "generated_at": "2026-10-11T14:30:05Z",
      "sl": 1.09500,
      "tps": [1.10500, 1.11000],
      "strategy": "my-strategy"
    }'
  ```

  ```bash curl (decisions) theme={null}
  curl "https://cloud.innova-trading.com/api/v1/provider/signals/decisions?limit=20" \
    -H "Authorization: Bearer <YOUR_TOKEN>" \
    -H "x-community-slug: <YOUR_COMMUNITY>"
  ```
</CodeGroup>

To send for real, use the same call and replace `/signals/test` with `/signals`.

<Note>
  Replace `<YOUR_TOKEN>` and `<YOUR_COMMUNITY>` with the values we give you.
  Never commit them to a repository.
</Note>

## Integration checklist

1. Ask your contact at Innova for the token and the `x-community-slug`.
2. Send a signal to `/signals/test`. Expect `"status": "simulated"`.
3. Trigger a rejection on purpose (send a `generated_at` from one hour ago)
   and confirm your code handles `code: "stale"`.
4. Send an invented field and confirm your code handles the `422`.
5. Check that your code retries with the same `signal_id` when a call fails (the Python kit, when available, does it for you). The sandbox always answers `simulated`: `duplicate` only shows up on the real `/signals`, so you will see it there for the first time. That is expected.
6. Only then switch to `/signals`.
7. Send us the `signal_source` from the response so we can subscribe accounts.

## Convert your PineScript indicator

<Note>
  **Coming soon.** This section and the next one describe the Python kit
  (`innova-signal-kit`), which is not downloadable yet. If you want early
  access, ask your contact at Innova. Until then, use the plain HTTP contract
  described above, which works today.
</Note>

If your strategy lives in TradingView as PineScript, you do not rewrite it by
hand. Everything comes in the Python kit (a pip package, see "Ready-made Python
kit" below). It includes a ready-made prompt
(`PROMPT-CONVERTIR-PINE.md`, copied into your folder by `innova-provider init`, or printed with `innova-provider prompt`) that you paste into an AI assistant together with
your Pine code, and it returns your `strategy.py` in the format the kit expects.

<Warning>
  **Innova does not receive your Pine code.** But the AI you paste it into does: a
  cloud assistant (Claude, ChatGPT or another) is a third party and receives your
  Pine. If your strategy is sensitive, use a local AI or review that service's
  privacy terms first.
</Warning>

1. Paste the prompt and your Pine code into your assistant, and save the result
   as `strategy.py`.
2. Export your candles and your indicator's signals from TradingView to two CSV files. The Strategy Tester trade list shows the time of the candle *after* the signal: in that case add `--expected-shift 1` to the command in step 3.
3. Run `innova-provider parity --bars bars.csv --expected expected.csv --symbol EURUSD --timeframe 15 --strategy-name my-strategy`.
4. **Acceptance criterion: 99 % of the signals match.** `--warmup` defaults to your
   `REQUIRED_BARS` (live, `evaluate` always starts with that full history): you do not need to
   pass it. **Check convergence:** run the tool with two values of `REQUIRED_BARS` (for example
   300 and 600) and keep the larger one if the result improves; the tool also prints an
   `AVISO:` when more history raises the match. Indicators with path-dependent state
   (supertrend, ATR/RMA/EMA, trailing stops) do not converge with "5 times the period":
   measured with a supertrend + RSI, `REQUIRED_BARS = 70` gave 94 % and `150` gave 100 %; start
   at 300. The tool answers `NO APROBADO: historia insuficiente` (even at 100 %) when the CSV has
   fewer than 4 times `REQUIRED_BARS` candles (the message gives the exact figure) or when fewer
   than 10 signals are compared. Your CSV must meet both conditions: at least 4 times
   `REQUIRED_BARS` candles AND at least 10 Pine signals in that stretch; if your Pine gives fewer,
   export more history until it does (on M15 with little activity that can take months); Pine signals before the first evaluated candle are ignored
   and only reported. If it still falls short, the tool lists every difference. Paste them back to your
   assistant and ask it to fix the conversion.
5. Only then move on to the sandbox (`/signals/test`) and, last, to `/signals`.

Some things do not convert on their own: indicators that read another timeframe
(`request.security`), logic inside the candle, and strategies that depend on an
open position. The prompt flags them as "Doubts" so you can decide.

## Ready-made Python kit

<Warning>
  **Coming soon, by request.** The kit is not published: there is no public
  `pip install` yet. Ask your contact at Innova for access. Do not rely on it
  for your integration today; use the HTTP contract (see "Prefer another
  language?").
</Warning>

We provide a Python kit (client with retries and latency measurement, a
runner that waits for each candle close, and a strategy template where you
only edit `strategy.py`). It is a pip package called `innova-signal-kit`, with a
single command: `innova-provider`.

### Installation

Python 3.9 or newer.

Installation instructions will be shared when you get access to the kit.
There is no public package index entry yet.

Your `strategy.py` is **not** part of the package: it lives in your own folder,
on your server, and Innova never receives it.

### Flow

```bash theme={null}
innova-provider init my-strategy      # creates the folder: strategy.py, config.env, README.md
cd my-strategy
# 1. fill in config.env (token and slug: we give them to you)
# 2. replace evaluate() in strategy.py with your logic
innova-provider parity --bars bars.csv --expected expected.csv \
    --symbol EURUSD --timeframe 15 --strategy-name my-strategy   # 99 % or more
innova-provider history --bars-csv bars.csv ...   # optional: your past signals (backtest-marked)
innova-provider run --dry-run --once  # sandbox: publishes nothing
innova-provider run                   # real
```

`run` accepts `--dry-run`, `--once`, `--env-file`, `--margin`, `--bars`,
`--fetch-attempts` and `-v`; `parity` accepts the options described above.
The `README` inside the kit explains everything.

### If the network fails

* The client makes up to 4 attempts per send (the first plus 3 retries). If it still does not arrive, the log says
  `SEÑAL PERDIDA` with the candle time and the `signal_id`.
* If the cause was the network or a 5xx, the runner retries it **once in the next cycle with
  the same `signal_id`** (the server does not duplicate), only while the candle is still within
  the 120 s freshness window (true on M1, not on longer candles; the log says so). A 401 or 422
  is not retried.
* After a long outage the log says `Se omitieron N velas sin evaluar (desde X hasta Y)`: the
  runner only evaluates the last closed candle, so signals of those candles are not sent
  and not replayed (they would arrive stale). A restart between a candle close and the next cycle
  also loses that candle.
* In `--dry-run` the 120 s candle-age guard is not applied: a test with the market closed passes
  and the same candle would be skipped in real mode. The reported `generated_at->received_at`
  latency includes the client retries.

### Your past signals (history)

```bash theme={null}
innova-provider history --bars-csv bars.csv --symbol EURUSD --timeframe 15 \
    --strategy-name my-strategy               # your last 50 past signals, from your own candles
innova-provider history --last 100            # candles downloaded from /api/external/bars
```

**What it is.** Your strategy runs **on your machine** over past candles and gives your last
N signals (50 by default, `--last N`). It uses the same window as live (`REQUIRED_BARS` closed
candles, the last one is the candle that just closed). Candles come from `--bars-csv` (same format
as `parity`) or, if you do not pass it, from `/api/external/bars` paging backwards with
`before_time` (up to 1000 per request, at most `--max-bars`, 10 000 by default; if that is not
enough for N signals the command says how many it found and why it stopped). The result is
`history.json` in your folder: each signal has only the contract fields plus `kind: "backtest"`
and `source: "kit-history"`; `generated_at` is the close of the signal's candle and the
`signal_id` starts with `backtest-`. The command also prints a summary (how many signals, date
range, buy/sell).

**What it is not.** It is not a backtest with PnL: nothing simulates SL or TP and there is no
equity curve; you only get which signals your strategy would have given. It is **always** marked
`backtest`, so it can never be mistaken for a live signal, and it is never mixed with the live
record. Your code and parameters do not leave your machine: only the contract fields travel.

**Known limit today.** The server accepts `before_time` but does not apply it yet, so downloading
more than one page of candles does not work: the command stops with a clear message. Use
`--bars-csv` with your own history.

**Sending it: automatic sending from the kit is not available yet.** The endpoint
`POST /api/v1/provider/signals/history` already exists on the server, but the kit's `--send`
flag is not available yet (`--send` fails with an error and sends nothing). If you post it
yourself over HTTP, do not send `history.json` as is: it has the top-level keys `kind`, `source`
and `summary`, and the endpoint rejects unknown keys with a 422 `extra_forbidden`. The body of
`POST /api/v1/provider/signals/history` is `{"signals": [ ... ]}`: only the array stored under the
`signals` key of `history.json`, without `kind`, `source` or `summary`. Send at most 50 signals per
request (split into batches if you have more; a longer list gets a 422 `too_long`). Each element
of that array carries the contract fields plus `kind: "backtest"` and `source`, which the endpoint
accepts per signal; in history `entry` must be a number.

### History limits

These limits apply to the history you send to `POST /api/v1/provider/signals/history` (the
endpoint the "Sending it" note above refers to).

| Limit | Default | What it controls |
| - | - | - |
| Retention | **90 days** | How old a past signal may be: its `generated_at` cannot be older than this. |
| Cap per provider | **500 signals** | How many history signals you can keep stored in total. |

**Each community can have its own values.** The Innova platform team can adjust both
limits for your community without a release (allowed range: retention 7 to 365 days, cap 50
to 5000 signals). The values in force for you come back in every response of the endpoint, in
the `retention_days` and `max_per_provider` fields: trust those, not the defaults above.

When a signal is not stored, its entry in `results` has `status: "rejected"` and one of these
`code`s. The rest of the batch is still processed.

| `code` | Why | What to do |
| - | - | - |
| `too_old` | `generated_at` is older than the retention window. | Send only signals inside the window (`retention_days` in the response). Older ones are not accepted; do not retry them. |
| `provider_cap_reached` | You already have `max_per_provider` history signals stored. Nothing is deleted for you. | Stop sending more. If you need more room, ask the Innova platform team to review your community's cap. Retrying the same signal changes nothing. |
| `generated_at_in_future` | `generated_at` is in the future, or your clock is ahead. | Check your machine's clock and the candle time you send. Fix the value and resend with the same `signal_id`. |

Signals you already sent are answered as `duplicate` even when you are at the cap, so
retrying a batch is always safe. Lowering a limit never deletes what is already stored.

### Prefer another language?

This is the path that works today. There is no JS or other SDK, and none is
promised: the contract is plain HTTP and works from any language. What you have to implement:

* **Endpoints** (see "Endpoints" above): read candles from
  `/api/external/bars`, send signals to `/api/v1/provider/signals/test`
  (sandbox) and then `/api/v1/provider/signals`.
* **Headers**: `Authorization: Bearer <token>` and `x-community-slug`.
* **Fields**: exactly those in "The JSON you send"; an unknown field is a `422`.
* **Stop and targets**: check on your side that `sl` and `tps` are on the right
  side of `entry`; the server does not check it.
* **`signal_id`**: deterministic (strategy + symbol + timeframe + candle time),
  and retry with the same one. See "Idempotency".
* **120 s guard**: generate the signal when the candle closes and only send it
  if that close was at most 120 s ago. `generated_at` is "now" and the server
  rejects it as `stale` past 120 s (see "Freshness"). Do not send the signal of
  an old candle (weekend, delayed data).
* **Candles**: use only closed ones; drop the one still forming.

The sections above (endpoints, JSON, idempotency, freshness, rejection codes)
are the contract reference. The Python kit is the reference implementation: you
can port its `runner` and `parity` module line by line.

## Next Steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/authentication">
    How API tokens work
  </Card>

  <Card title="Signal Best Practices" icon="check" href="/guides/signal-best-practices">
    Tips for better signals
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.