Recettes de webhooks
Les alertes vous disent ce que fait le marché. Les webhooks et Telegram vous permettent d’agir, avec vos propres outils et vos propres comptes. Nous ne tradons jamais pour vous, ne détenons jamais de fonds et ne demandons jamais de clés API autorisant le trading.
Avant de commencer
- Sur la page Alertes, ajoutez un webhook JSON personnalisé. Inclus dans VIP Quant et l’essai.
- Copiez le secret de signature. Il n’est affiché qu’une fois.
- Appuyez sur Envoyer un test et vérifiez que votre endpoint le reçoit.
- Dirigez une règle vers la destination. L’alerte par défaut y est aussi envoyée tant qu’elle est active.
Répondez par un code 2xx dans les 8 secondes. Les envois échoués sont relancés avec le même id : dédoublonnez dessus. Chaque extrait ci-dessous a été testé sur un véritable envoi signé. Vous êtes responsable de tout ce que vous automatisez à partir de ces événements.
Événements
opportunity.alert: une route correspond à une règle ou à l’alerte par défaut.rule.idvautdefaultpour l’alerte par défaut.listing.alert: un marché vient d’être listé, avec un événement par listing.opportunity.closed: une route pour laquelle vous avez été alerté s’est fermée, si la règle le demande.position.alert: un événement s’est produit sur un trade suivi.notice.alert: une plateforme a annoncé un delisting, une suspension des dépôts ou retraits, une maintenance ou un listing, sur l’Alerte par défaut.trackedindique qu’il s’agit d’un trade que vous suivez.watch.alert: un wallet que vous surveillez a reçu ou envoyé un transfert, sur l’Alerte par défaut.event.txest null quand le hash de la transaction n’est pas connu.alert.test: depuis Envoyer un test, avec"test": trueet une fausse crypto nommée TEST.
{
"id": "evt_7c1e...",
"type": "listing.alert",
"test": false,
"sentAt": "2026-11-02T10:00:00.000Z",
"rule": { "id": "...", "name": "New listings" },
"listing": { "coin": "ABC", "venue": "Binance", "kind": "spot", "symbol": "ABCUSDT", "listedMs": 1793000000000 },
"window": { "sizeUsd": 1000, "openMs": 4000, "avgWindowMs": null, "avgPeakNetBps": null, "windowSamples": 0 },
"opportunity": { "id": "ABC|Binance:spot|OKX:spot", "coin": "ABC", "...": "..." }
}Les champs complets figurent dans la documentation des webhooks.
Vérifier la signature
Chaque requête contient X-ArbitrageX-Timestamp et X-ArbitrageX-Signature, qui vaut sha256= suivi du HMAC-SHA256 hexadécimal de timestamp.rawBody. Vérifiez-la sur les octets bruts, avant le parsing, car un JSON parsé puis réencodé ne correspondra pas. Refusez les timestamps à plus de 5 minutes de l’heure actuelle, pour bloquer les rejeux.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, headers, secret) {
const ts = headers["x-arbitragex-timestamp"];
if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= 300)) return false;
const expected =
"sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const given = Buffer.from(String(headers["x-arbitragex-signature"]));
const wanted = Buffer.from(expected);
return given.length === wanted.length && timingSafeEqual(given, wanted);
}Python
import hashlib
import hmac
import time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
ts = headers.get("x-arbitragex-timestamp", "")
try:
if abs(time.time() - int(ts)) > 300:
return False
except ValueError:
return False
signed = ts.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), headers.get("x-arbitragex-signature", "").encode())Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"math"
"net/http"
"strconv"
"time"
)
func Verify(rawBody []byte, headers http.Header, secret string) bool {
ts := headers.Get("X-ArbitrageX-Timestamp")
unix, err := strconv.ParseInt(ts, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-unix)) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts + "."))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(headers.Get("X-ArbitrageX-Signature")))
}Outils d’exécution
Chaque exemple ci-dessous est le corps qu’attend un outil, écrit à partir de notre payload standard : chaque valeur entre chevrons est le champ de notre payload à y placer. Construisez-le dans une étape Zapier ou Make après un catch webhook, ou dans votre propre relais comme le récepteur de la section Lancer votre propre script, et envoyez-le à l’URL de webhook de l’outil. Les valeurs en majuscules sont les vôtres. Seuls opportunity.alert et listing.alert doivent atteindre un outil : filtrez test et tous les autres types, pour qu’un suivi de clôture ne puisse pas ouvrir de trade. Nous ne tradons jamais : ce qu’un outil fait d’un message dépend de vous et de l’outil.
Chaque outil fait évoluer ses propres champs au fil du temps. Comparez un exemple à la documentation actuelle de l’outil avant de vous y fier.
3Commas signal bot
Pour le webhook personnalisé d’un signal bot 3Commas, avec son secret et l’UUID du bot tirés des paramètres de message du bot. Un signal bot trade sur un seul exchange : limitez donc la règle aux routes qui achètent sur celui-ci avec le filtre d’exchange de la règle.
{
"secret": "YOUR_3COMMAS_SECRET",
"bot_uuid": "YOUR_BOT_UUID",
"max_lag": "300",
"timestamp": "<sentAt>",
"trigger_price": "<opportunity.buy.price>",
"tv_exchange": "<opportunity.buy.venue in capitals, e.g. BINANCE>",
"tv_instrument": "<opportunity.coin>USDT",
"action": "enter_long"
}Hummingbot
Hummingbot reçoit des commandes via ses propres interfaces plutôt que par webhook : ceci est donc destiné à une petite stratégie script de votre cru, ou à une passerelle vers son interface MQTT. Utilisez le nom de votre propre connecteur.
{
"connector": "<opportunity.buy.venue in lower case, e.g. binance>",
"trading_pair": "<opportunity.coin>-USDT",
"side": "BUY",
"price": <opportunity.buy.price>,
"amount_usd": <window.sizeUsd>,
"hedge_connector": "<opportunity.sell.venue in lower case>",
"hedge_price": <opportunity.sell.price>,
"net_bps": <opportunity.netBps>,
"event_id": "<id>"
}Cornix signal
Du texte brut au format de signal de Cornix, pour l’adresse de webhook que Cornix fournit à un bot TradingView. Définissez votre propre stop, et comparez le format au guide actuel de Cornix.
#<opportunity.coin>/USDT Exchanges: <opportunity.buy.venue> Signal Type: Regular (Long) Entry Targets: 1) <opportunity.buy.price> Take-Profit Targets: 1) <opportunity.sell.price> Stop Targets: 1) YOUR_STOP_PRICE
TradingView-style JSON
Les noms de champs qu’envoie une alerte TradingView, pour les outils conçus pour recevoir des alertes TradingView.
{
"ticker": "<opportunity.coin>USDT",
"exchange": "<opportunity.buy.venue in capitals>",
"action": "buy",
"price": <opportunity.buy.price>,
"time": "<sentAt>",
"message": "<opportunity.coin> <opportunity.buy.venue> to <opportunity.sell.venue>, net <opportunity.netBps> bps"
}Flat JSON
Chaque chiffre au premier niveau, pour un tableur, n8n ou une base de données.
{
"id": "<id>",
"coin": "<opportunity.coin>",
"net_bps": <opportunity.netBps>,
"net_usd_1k": <opportunity.netUsd.1000>,
"max_size_usd": <opportunity.maxSizeUsd>,
"buy_exchange": "<opportunity.buy.venue>",
"buy_price": <opportunity.buy.price>,
"buy_url": "<opportunity.buy.url>",
"sell_exchange": "<opportunity.sell.venue>",
"sell_price": <opportunity.sell.price>,
"sell_url": "<opportunity.sell.url>"
}Autres canaux
Chaque canal ci-dessous se configure sur la page Alertes, dispose d’un bouton Envoyer un test et se lit comme le message Telegram, dans la langue du compte. Les règles, les délais de récupération, les heures de silence, la limite de débit et la pause automatique fonctionnent de la même façon sur tous.
- Slack (Pro Desk et au-delà) : ajoutez l’app Incoming Webhooks à un canal dans Slack et collez l’URL
hooks.slack.comqu’elle vous donne. - ntfy (Starter et au-delà) : des notifications gratuites sur téléphone et ordinateur. Collez l’URL d’un topic comme
https://ntfy.sh/your-topic, ou d’un topic sur votre propre serveur, et abonnez-vous au même topic dans l’app ntfy. Toute personne qui connaît le nom d’un topic public peut le lire : choisissez-en un long et aléatoire, ou ajoutez un jeton d’accès pour un topic protégé. Chaque alerte comporte des boutons pour les deux exchanges. - Résumé par email (Starter et au-delà) : un email à l’adresse de votre compte avec toutes les alertes depuis le précédent, toutes les heures pile ou chaque jour à 08:00 UTC. Rien n’est envoyé quand il n’y a rien de nouveau.
WhatsApp n’offre aucun moyen gratuit pour qu’un service vous envoie des messages, il n’est donc pas proposé. Pour tout le reste, un webhook personnalisé est du JSON simple via https.
Publier sur Slack
Un Cloudflare Worker, sans serveur à gérer. Définissez ARBITRAGEX_SECRET et SLACK_WEBHOOK_URL comme secrets, puis utilisez l’URL du Worker comme destination. Le même code fonctionne partout où Web Crypto est disponible.
async function verify(rawBody, headers, secret) {
const ts = headers.get("x-arbitragex-timestamp");
if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= 300)) return false;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"],
);
const given = (headers.get("x-arbitragex-signature") ?? "").replace("sha256=", "");
if (!/^[0-9a-f]{64}$/.test(given)) return false;
const bytes = Uint8Array.from(given.match(/../g), (pair) => parseInt(pair, 16));
return crypto.subtle.verify("HMAC", key, bytes, encoder.encode(`${ts}.${rawBody}`));
}
export default {
async fetch(request, env) {
const rawBody = await request.text();
if (!(await verify(rawBody, request.headers, env.ARBITRAGEX_SECRET))) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(rawBody);
if (event.type === "opportunity.alert" || event.type === "listing.alert") {
const { coin, buy, sell, netBps } = event.opportunity;
await fetch(env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `${coin}: ${buy.venue} to ${sell.venue}, net ${netBps.toFixed(1)} bps`,
}),
});
}
return new Response(null, { status: 204 });
},
};Tenir un tableur
Ajoute une ligne par alerte à un CSV que vous pouvez ouvrir dans n’importe quel tableur, avec la seule bibliothèque standard de Python. Placez-le derrière https : nous n’envoyons qu’à des adresses https publiques.
import csv
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
import hashlib
import hmac
import time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
ts = headers.get("x-arbitragex-timestamp", "")
try:
if abs(time.time() - int(ts)) > 300:
return False
except ValueError:
return False
signed = ts.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), headers.get("x-arbitragex-signature", "").encode())
SECRET = "your signing secret"
seen = set()
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
raw_body = self.rfile.read(int(self.headers["Content-Length"]))
headers = {key.lower(): value for key, value in self.headers.items()}
if not verify(raw_body, headers, SECRET):
self.send_response(401)
self.end_headers()
return
self.send_response(204)
self.end_headers()
event = json.loads(raw_body)
if event["test"] or event["id"] in seen or "opportunity" not in event:
return
seen.add(event["id"])
row = event["opportunity"]
with open("alerts.csv", "a", newline="") as file:
csv.writer(file).writerow(
[event["sentAt"], event["type"], row["coin"], row["buy"]["venue"], row["sell"]["venue"], row["netBps"]]
)
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8000), Receiver).serve_forever()Lancer votre propre script
Un récepteur sans dépendance, qui répond immédiatement et transmet chaque alerte à une de vos fonctions. Ce que fait cette fonction, c’est à vous de l’écrire et d’en répondre. Les alertes sont des estimations issues de données de marché, et une route peut se fermer avant que ce que vous lancez n’arrive.
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, headers, secret) {
const ts = headers["x-arbitragex-timestamp"];
if (!(Math.abs(Date.now() / 1000 - Number(ts)) <= 300)) return false;
const expected =
"sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const given = Buffer.from(String(headers["x-arbitragex-signature"]));
const wanted = Buffer.from(expected);
return given.length === wanted.length && timingSafeEqual(given, wanted);
}
const seen = new Set();
function onAlert(event) {
// Your code goes here. You decide what happens next, and we never see it.
console.log(event.type, event.opportunity.coin, event.opportunity.netBps);
}
export const server = createServer((req, res) => {
const parts = [];
req.on("data", (part) => parts.push(part));
req.on("end", () => {
const rawBody = Buffer.concat(parts).toString("utf8");
if (!verify(rawBody, req.headers, process.env.ARBITRAGEX_SECRET)) {
return res.writeHead(401).end();
}
const event = JSON.parse(rawBody);
// Answer first: anything slower than 8 seconds counts as a failed delivery.
res.writeHead(204).end();
if (event.test || seen.has(event.id)) return;
seen.add(event.id);
if (event.type === "opportunity.alert" || event.type === "listing.alert") onAlert(event);
});
});Zapier et Make
Un webhook personnalisé est un simple POST JSON : une URL de webhook de réception de l’un ou l’autre fonctionne comme destination. Ajoutez une étape de filtre sur type et test, puis mappez des champs comme opportunity.coin et opportunity.netBps. Ces étapes ne vérifient pas notre signature à votre place : traitez l’URL de réception comme un mot de passe et n’y déclenchez rien que vous ne laisseriez pas un inconnu déclencher.
Telegram
Si vous voulez seulement des alertes sur votre téléphone, ignorez tout ceci. Appuyez sur Connecter Telegram sur la page Alertes, puis sur Démarrer dans la conversation qui s’ouvre. Consultez la documentation des alertes.