Skip to main content

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)
Store the signing secret immediately

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

EventWhen
settlement.createdpayment / settlement initiated
settlement.completedsettlement validated
settlement.failedpayment failed
counterparty.kyb_approvedparticipant KYB passed
counterparty.kyb_rejectedparticipant KYB failed
on_ramp.completedon-ramp leg done
off_ramp.completedoff-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"
Never log the secret

Redact webhook_secret / secret everywhere. Treat raw payloads as untrusted until the signature verifies.

Next steps