Capítulo 1Añadir un endpoint
- En API y webhooks, pulsa «Añadir endpoint»
- Escribe la URL de tu servidorTiene que ser https y pública (no localhost ni IPs internas). No seguimos redirecciones.
- Elige los eventos«Todos los eventos», o los que quieras. También vale un comodín por grupo, como
milestone.*. - Copia el secreto de firmaGuárdalo como
KROWS_WEBHOOK_SECRET. - Pulsa «Enviar evento de prueba»Llega un
test.ping. Mira el resultado en el registro.
Cada endpoint es de un modo: prueba o real, según el selector.
Capítulo 2Verificar la firma
Cada envío es un POST con Content-Type: application/json, User-Agent: Krows-Webhooks/1.0 y la cabecera Krows-Signature: t=<marca de tiempo>,v1=<firma>. La firma es HMAC-SHA256 en hexadecimal de t + "." + cuerpo con el secreto de tu endpoint. Usa el cuerpo en bruto, tal cual llega.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Use the raw body: the signature is computed over the exact bytes we send
app.post('/webhooks/krows', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('Krows-Signature') ?? '';
const { t, v1 } = Object.fromEntries(header.split(',').map((x) => x.split('=')));
const expected = crypto.createHmac('sha256', process.env.KROWS_WEBHOOK_SECRET)
.update(`${t}.${req.body}`).digest('hex');
const valid = v1 && v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
// Reject anything older than 5 minutes to stop replays
if (!valid || Date.now() / 1000 - Number(t) > 300) return res.sendStatus(400);
const event = JSON.parse(req.body);
switch (event.type) {
case 'investment.confirmed':
// event.data is the investment: amount, tokens, investor, reference, metadata…
break;
case 'round.succeeded':
break;
}
res.sendStatus(200); // answer 2xx within 10 seconds
});import hmac, hashlib, os, time, json
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["KROWS_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/krows")
def krows():
header = request.headers.get("Krows-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
body = request.get_data() # raw bytes
expected = hmac.new(SECRET, f"{parts.get('t')}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(parts.get("v1", ""), expected) or time.time() - int(parts.get("t", 0)) > 300:
abort(400)
event = json.loads(body)
if event["type"] == "investment.confirmed":
pass # event["data"] is the investment
return "", 200<?php
$secret = getenv('KROWS_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_KROWS_SIGNATURE'] ?? ''), $p);
$expected = hash_hmac('sha256', ($p['t'] ?? '') . '.' . $body, $secret);
if (!hash_equals($expected, $p['v1'] ?? '') || time() - (int)($p['t'] ?? 0) > 300) {
http_response_code(400); exit;
}
$event = json_decode($body, true);
if ($event['type'] === 'investment.confirmed') {
// $event['data'] is the investment
}
http_response_code(200);{
"id": "evt_Qm3x…",
"object": "event",
"type": "investment.confirmed",
"mode": "live",
"created": "2026-10-17T10:03:13.000Z",
"data": {
"object": "investment",
"id": "inv_…",
"status": "confirmed",
"amount": 250,
"currency": "USDC",
"tokens": 250,
"investor": { "alias": "ana_investor", "wallet": "7xKX…gAsU" },
"reference": "order-1234",
"metadata": { "campaign": "autumn" },
"transaction": "5hTq…"
}
}Capítulo 3Respuestas y reintentos
- Responde 2xx en menos de 10 segundos. Si tienes trabajo largo, encólalo y responde ya.
- Si falla, reintentamos a 1 minuto, 5 minutos, 30 minutos, 2 horas, 6 horas y 24 horas: hasta 6 reintentos.
- Puede llegar el mismo evento más de una vez: guarda el
iddel evento y procésalo una sola vez. - El orden no está garantizado: usa
createdy consulta la API si lo necesitas. - Si te perdiste algo,
GET /v1/eventsdevuelve el historial.
Capítulo 4Todos los eventos
| Evento | Cuándo |
|---|---|
| investment.confirmed | Alguien ha invertido |
| investment.withdrawn | Un inversor retira su inversión |
| checkout.session.completed | Se completa una sesión creada por API |
| round.succeeded | La ronda llega al 100 % |
| round.failed | La ronda no llega o se cancela |
| tokens.delivered | Un inversor recibe sus tokens |
| refund.completed | Se devuelve el USDC a un inversor |
| milestone.requested | Pides liberar un hito y se abre la votación |
| milestone.approved | Los inversores aprueban un hito y se libera el dinero |
| milestone.rejected | Los inversores rechazan un hito |
| report.delivered | Entregas un informe |
| referendum.opened | Se abre una votación para cerrar la startup |
| referendum.closed | Termina la votación de cierre |
| free_withdrawal.requested | Pides una retirada libre y empieza el plazo de veto |
| free_withdrawal.paid | Nadie la veta y el contrato te paga |
| free_withdrawal.vetoed | Los inversores la vetan |
| test.ping | Evento de prueba desde el panel |
¿Te ha ayudado?
Gracias. Lo tendremos en cuenta para mejorar.