Recetas de webhooks
Las alertas te dicen qué está haciendo el mercado. Los webhooks y Telegram son la forma de actuar, con tus propias herramientas y tus propias cuentas. Nunca operamos por ti, nunca custodiamos fondos y nunca pedimos claves API con permiso de trading.
Antes de empezar
- En la página de Alertas, añade un webhook JSON personalizado. VIP Quant y la prueba lo incluyen.
- Copia el secreto de firma. Se muestra una sola vez.
- Pulsa Enviar prueba y comprueba que tu endpoint la recibe.
- Apunta una regla al destino. La alerta por defecto también le llega mientras esté activa.
Responde con cualquier 2xx en 8 segundos. Los envíos fallidos se reintentan con el mismo id, así que deduplica por él. Cada fragmento de abajo se probó con un envío firmado real. Eres responsable de todo lo que automatices a partir de estos eventos.
Eventos
opportunity.alert: una ruta cumplió una regla o la alerta por defecto.rule.idesdefaultpara la alerta por defecto.listing.alert: un mercado acaba de listarse, con un evento por listado.opportunity.closed: una ruta de la que se te alertó se ha cerrado, si la regla lo pide.position.alert: algo ocurrió en una operación seguida.notice.alert: un exchange anunció un delisting, una suspensión de depósitos o retiros, un mantenimiento o un listado, en la Alerta por defecto.trackedindica que es una operación que sigues.watch.alert: una wallet que vigilas recibió o envió una transferencia, en la Alerta por defecto.event.txes null cuando no se conoce el hash de la transacción.alert.test: desde Enviar prueba, con"test": truey una moneda ficticia llamada 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", "...": "..." }
}Los campos completos están en la documentación de webhooks.
Comprobar la firma
Cada petición lleva X-ArbitrageX-Timestamp y X-ArbitrageX-Signature, que es sha256= más el HMAC-SHA256 en hex de timestamp.rawBody. Compruébalo sobre los bytes sin procesar, antes de parsear, ya que un JSON parseado y recodificado no coincidirá. Rechaza marcas de tiempo a más de 5 minutos de ahora para evitar repeticiones.
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")))
}Herramientas de ejecución
Cada ejemplo de abajo es el cuerpo que espera una herramienta, escrito sobre nuestro payload estándar: cada valor entre corchetes angulares es el campo nuestro que va ahí. Constrúyelo en un paso de Zapier o Make tras un webhook de captura, o en tu propio relé, como el receptor de Ejecutar tu propio script, y envíalo a la URL del webhook de la herramienta. Los valores en mayúsculas son tuyos. Solo opportunity.alert y listing.alert deben llegar a una herramienta: filtra test y cualquier otro tipo, para que un aviso de cierre no pueda abrir una operación. Nunca operamos: lo que una herramienta haga con un mensaje depende de ti y de la herramienta.
Cada herramienta cambia sus propios campos con el tiempo. Comprueba un ejemplo con la documentación actual de la herramienta antes de confiar en él.
3Commas signal bot
Para el webhook personalizado de un signal bot de 3Commas, con su secreto y el UUID del bot de la configuración de mensajes del bot. Un signal bot opera en un solo exchange, así que limita la regla a las rutas que compran ahí con el filtro de exchange de la regla.
{
"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 recibe órdenes por sus propias interfaces y no por un webhook, así que esto va a una pequeña estrategia de script tuya, o a un puente hacia su interfaz MQTT. Usa el nombre de tu propio conector.
{
"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
Texto plano con el formato de señal de Cornix, para la dirección de webhook que Cornix da a un bot de TradingView. Pon tu propio stop y comprueba el formato con la guía actual 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
Los nombres de campo que envía una alerta de TradingView, para herramientas hechas para recibir alertas de 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
Todas las cifras en el nivel superior, para una hoja de cálculo, n8n o una base de datos.
{
"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>"
}Otros canales
Cada canal de abajo se configura en la página de Alertas, tiene un botón Enviar prueba y se lee como el mensaje de Telegram, en el idioma de la cuenta. Las reglas, los periodos de enfriamiento, las horas de silencio, el límite de envíos y la pausa automática funcionan igual en todos.
- Slack (Pro Desk y superiores): añade la app Incoming Webhooks a un canal de Slack y pega la URL
hooks.slack.comque te da. - ntfy (Starter y superiores): notificaciones gratis en el móvil y el escritorio. Pega la URL de un tema como
https://ntfy.sh/your-topic, o uno en tu propio servidor, y suscríbete al mismo tema en la app ntfy. Cualquiera que sepa el nombre de un tema público puede leerlo, así que elige uno largo y aleatorio, o añade un token de acceso para un tema protegido. Cada alerta tiene botones para ambos exchanges. - Resumen por email (Starter y superiores): un correo a la dirección de tu cuenta con todas las alertas desde el anterior, cada hora en punto o cada día a las 08:00 UTC. No se envía nada cuando no hay nada nuevo.
WhatsApp no ofrece ninguna forma gratuita de que un servicio te escriba, así que no está disponible. Para todo lo demás, un webhook personalizado es JSON simple sobre https.
Publicar en Slack
Un Cloudflare Worker sin servidor que mantener. Define ARBITRAGEX_SECRET y SLACK_WEBHOOK_URL como secretos y usa la URL del Worker como destino. El mismo código funciona en cualquier entorno con Web Crypto.
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 });
},
};Llevar una hoja de cálculo
Añade una línea por alerta a un CSV que puedes abrir en cualquier hoja de cálculo, solo con la biblioteca estándar de Python. Ponlo detrás de https: solo enviamos a direcciones https públicas.
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()Ejecutar tu propio script
Un receptor sin dependencias, que responde al instante y pasa cada alerta a una función tuya. Lo que haga esa función lo escribes tú y respondes tú de ello. Las alertas son estimaciones a partir de datos de mercado, y una ruta puede cerrarse antes de que llegue cualquier cosa que inicies.
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 y Make
Un webhook personalizado es un POST JSON simple, así que una URL de webhook de captura de cualquiera de los dos sirve como destino. Añade un paso de filtro sobre type y test y luego mapea campos como opportunity.coin y opportunity.netBps. Estos pasos no comprueban nuestra firma por ti, así que trata la URL de captura como una contraseña y no inicies ahí nada que no dejarías iniciar a un desconocido.
Telegram
Si solo quieres alertas en el móvil, sáltate todo esto. Pulsa Conectar Telegram en la página de Alertas y luego Start en el chat que se abre. Consulta la documentación de alertas.