The key
Keys are issued in the cabinet, in the "Integrations" section. A key is shown once: the database holds only its hash, and showing it again is impossible even for an administrator. Lost it — revoke and issue a new one.
The key goes in a header. It is deliberately not accepted in the query string: URLs end up in proxy logs, in browser history and in other sites' Referer.
curl -s https://123-opt.com/api/v1/me \
-H "Authorization: Bearer sk_live_YOUR_KEY"
Every key has its own hourly request cap. Exceeding it returns 429 with a Retry-After header.
What you can fetch
# Your signals: the last 50, narrowed by instrument and date
curl -s "https://123-opt.com/api/v1/signals?limit=50&symbol=EURUSD" \
-H "Authorization: Bearer $KEY"
# Only what is new since last time
curl -s "https://123-opt.com/api/v1/signals?since=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer $KEY"
# Your strategies
curl -s https://123-opt.com/api/v1/strategies -H "Authorization: Bearer $KEY"
# Statistics for a period
curl -s "https://123-opt.com/api/v1/stats?days=90" -H "Authorization: Bearer $KEY"
The API returns only your data. There is no route here that shows anyone else's.
What a signal contains
{
"id": "…",
"symbol": "EURUSD",
"direction": "up",
"timeframe": "1h",
"horizonMinutes": 60,
"score": 5.5,
"generatedAt": "2026-09-10T12:00:03.120Z",
"barOpenAt": "2026-09-10T11:00:00.000Z",
"outcome": { "movePct": 0.42, "correct": true, "resolvedAt": "…" }
}
There is no entry price, and that is not an omission: a signal claims a direction over a horizon, and the price depends on when you press the button at your broker. outcome stays empty until the horizon ends.
Webhook
Instead of polling you can receive a POST on every new signal. The URL is set in the same section of the cabinet. Requirements: https only, port 443, no credentials in the URL, and the address must be public — internal networks are rejected.
Request body:
{
"event": "signal.created",
"signal": { "id": "…", "symbol": "EURUSD", "direction": "up" }
}
Headers:
X-Signal-Timestamp— send time, Unix seconds;X-Signal-Signature— HMAC-SHA256 in hex;X-Signal-Delivery,X-Signal-Attempt— for debugging retries.
Verifying the signature
The string {timestamp}.{body} is signed with your secret. The timestamp is part of the signature on purpose: without it an intercepted request could be replayed indefinitely.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(secret, rawBody, headers) {
const timestamp = Number(headers['x-signal-timestamp'])
// Older than five minutes — reject: this is replay protection.
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const expected = createHmac('sha256', secret)
.update(timestamp + '.' + rawBody)
.digest('hex')
const got = headers['x-signal-signature']
if (expected.length !== got.length) return false
return timingSafeEqual(Buffer.from(expected), Buffer.from(got))
}
import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, ts: str, sig: str) -> bool:
if abs(time.time() - int(ts)) > 300:
return False
expected = hmac.new(
secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, sig)
Sign the raw body, before parsing JSON: text reassembled from an object differs in whitespace and field order, and the signature will not match.
Retries
A 2xx response means "accepted". Anything else means a retry: after 1, 5, 25, 120 and 360 minutes. After the fifth failure the delivery is marked failed, and it is visible in the call log in the cabinet.
If an address fails many times in a row the subscription is switched off — hammering a dead address is pointless. You turn it back on in the same place.
Answer quickly. The service waits ten seconds; long processing on your side belongs after the response, not before it.