Webhooks
Subscribe to HTTPS endpoints to receive signed event deliveries with retries.
Register an endpoint
WEBHOOK_ID=$(curl -s -X POST "$BASE_URL/v1/webhooks" \
-H "X-API-Key: $THIQWAVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.partner.com/webhooks/thiqwave",
"description": "Production settlement callbacks",
"events": ["settlement.completed", "settlement.failed", "counterparty.kyb_approved"]
}' | jq -r .id)
The plaintext secret is returned once on create (and on rotate). It is encrypted at rest and never shown again. Store it in a secret manager. If lost, rotate via POST /v1/webhooks/:id/secret/rotate.
Events
| Event | When |
|---|---|
settlement.created | payment / settlement initiated |
settlement.completed | settlement validated |
settlement.failed | payment failed |
counterparty.kyb_approved | participant KYB passed |
counterparty.kyb_rejected | participant KYB failed |
on_ramp.completed | on-ramp leg done |
off_ramp.completed | off-ramp leg done |
Payload shape (representative):
{
"event": "settlement.completed",
"payment_id": "01970000-0000-7000-8000-eeeeeeeeeeee",
"timestamp": "2026-08-05T12:01:00.000Z",
"data": { "status": "SETTLED", "settled_at": "2026-08-05T12:01:00.000Z" }
}
Verify the signature (verified scheme)
Each delivery carries:
X-Thiqwave-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
X-Thiqwave-Event: <event_type>
X-Request-ID: <correlation_id>
User-Agent: Thiqwave-Webhook/1.0
Content-Type: application/json
v1 is HMAC_SHA256(secret, "<unix_seconds>.<raw_request_body>") in lowercase hex. The signed string is ${timestamp}.${rawBody} (dot-separated, timestamp prefix). Verify against the raw body before any JSON re-serialisation.
const crypto = require('crypto');
function verify(secret, rawBody, sigHeader, toleranceSeconds = 300) {
const parts = Object.fromEntries(sigHeader.split(',').map(s => s.split('=')));
if (!parts.t || !parts.v1) return false;
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; // 5-min replay window
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, sig_header: str, tolerance: int = 300) -> bool:
parts = dict(p.split('=', 1) for p in sig_header.split(','))
t, v1 = parts.get('t'), parts.get('v1')
if not (t and v1) or abs(int(time.time()) - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
Respond 2xx fast; process async. Retries use exponential backoff (1s base, up to 10 attempts). Make your receiver idempotent (key on event id).
Manage endpoints
curl "$BASE_URL/v1/webhooks" -H "X-API-Key: $THIQWAVE_API_KEY"
curl -X PATCH "$BASE_URL/v1/webhooks/$WEBHOOK_ID" \
-H "X-API-Key: $THIQWAVE_API_KEY" -H "Content-Type: application/json" \
-d '{ "events": ["settlement.completed"] }'
curl -X DELETE "$BASE_URL/v1/webhooks/$WEBHOOK_ID" -H "X-API-Key: $THIQWAVE_API_KEY"
curl -X POST "$BASE_URL/v1/webhooks/$WEBHOOK_ID/secret/rotate" -H "X-API-Key: $THIQWAVE_API_KEY"
Redact webhook_secret / secret everywhere. Treat raw payloads as untrusted until the signature verifies.