Schema payload webhook (v1)
Schema JSON versionato, header e snippet di verifica in Node, Python e PHP.
Header
`X-Botely-Signature: sha256=<hex>` — HMAC-SHA256 del body RAW usando il tuo secret webhook. Sempre hex minuscolo.
`X-Botely-Timestamp: <unix-ms>` — quando Botely ha firmato la richiesta. Rifiuta tutto ciò che è più vecchio di ~5 minuti per difendere da replay.
`X-Botely-Event-Id: <delivery-id>` — unico per ogni tentativo. Usalo come idempotency key; lo stesso evento logico condivide `signalId` ma cambia event-id tra i retry.
Forma del body
Campi top-level: `version` (per ora sempre `"1"`), `eventId`, `signalId`, `clientEventId`, `strategyVersion` (`v6.4`/`v7`), `configHash`, `market`, `action` (`OPEN`/`CLOSE`/`TEST`), `side` (`LONG`/`SHORT`/null), `generatedAt` (ISO).
Per OPEN: oggetto `open` con `suggestedPriceLow`, `suggestedPriceHigh`, `suggestedAllocPct`, `tpPct`, `slPct`, `maxHoldHours`, `reasonCode` (es. `NR`, `3bc+red+atr_bd+dc`).
Per CLOSE: oggetto `close` con `opensSignalId` (signalId dell'OPEN), `closeReason` (`TP`/`SL`/`MH`/`FLAT`/`RE`/`EL`/`CLOSE_OPP`/`admin_close`/`cmd_close`/`flip`), `pnlPct`.
Receiver Node (Express)
```js import crypto from 'node:crypto'; import express from 'express'; const app = express(); const SECRET = process.env.BOTELY_WEBHOOK_SECRET; app.post('/botely-webhook', express.raw({ type: '*/*' }), (req, res) => { const sig = req.header('X-Botely-Signature') || ''; const provided = sig.replace(/^sha256=/, ''); const expected = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex'); if (provided.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(provided, 'hex'), Buffer.from(expected, 'hex'))) { return res.status(401).end(); } const ts = Number(req.header('X-Botely-Timestamp')); if (Math.abs(Date.now() - ts) > 5 * 60_000) return res.status(401).end(); const event = JSON.parse(req.body.toString('utf8')); // il tuo codice qui res.json({ ok: true }); }); ```
Receiver Python (Flask)
```py import hashlib, hmac, time, json from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ['BOTELY_WEBHOOK_SECRET'].encode() @app.post('/botely-webhook') def receive(): sig = request.headers.get('X-Botely-Signature', '').removeprefix('sha256=') expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expected): abort(401) ts = int(request.headers.get('X-Botely-Timestamp', '0')) if abs(time.time()*1000 - ts) > 5*60*1000: abort(401) event = json.loads(request.get_data()) # il tuo codice qui return {'ok': True} ```
Receiver PHP
```php <?php $secret = getenv('BOTELY_WEBHOOK_SECRET'); $raw = file_get_contents('php://input'); $sig = $_SERVER['HTTP_X_BOTELY_SIGNATURE'] ?? ''; $provided = preg_replace('/^sha256=/', '', $sig); $expected = hash_hmac('sha256', $raw, $secret); if (!hash_equals($provided, $expected)) { http_response_code(401); exit; } $event = json_decode($raw, true); // il tuo codice qui header('Content-Type: application/json'); echo json_encode(['ok' => true]); ```