Appearance
Webhooks
Webhooks push swap lifecycle events to an endpoint you register, so you do not have to poll. Delivery is signed and retried, which makes it the right choice for server-to-server integration.
Registering an endpoint
Management routes require a bearer token issued by the platform operator:
Authorization: Bearer $BRIDGE_ADMIN_TOKENbash
curl -X POST https://bridge.testnet.kriptonyx.com/api/webhook/subscriptions \
-H "Authorization: Bearer $BRIDGE_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"url": "https://your-service.example.com/knyx-hooks",
"event_types": ["source_token_deposited", "target_token_sent", "swap_failed"],
"description": "Settlement listener"
}'The secret is shown once
The response contains a signing secret that is never returned again. Store it before closing the response. If it is lost, rotate rather than re-register.
Omit event_types to receive everything. A trailing * is a prefix match, so simpleswap_* covers every SimpleSwap state without naming each one.
Destination URLs must be http or https, and may not point at loopback or private addresses unless the operator has permitted it.
Managing subscriptions
| Method | Path | Purpose |
|---|---|---|
POST | /api/webhook/subscriptions | Register an endpoint |
GET | /api/webhook/subscriptions | List. Secrets are never returned |
GET | /api/webhook/subscriptions/:id | One subscription, with failure counters |
PATCH | /api/webhook/subscriptions/:id | Change URL, events, method, description or active state |
DELETE | /api/webhook/subscriptions/:id | Remove it and its queued deliveries |
POST | /api/webhook/subscriptions/:id/rotate-secret | Issue a new secret |
POST | /api/webhook/subscriptions/:id/test | Send a signed test_ping |
GET | /api/webhook/subscriptions/:id/deliveries | Delivery history |
GET | /api/webhook/event-types | Catalogue. No authentication |
POST | /api/webhook/retry/:eventId | Force an immediate retry |
PATCH changes only the fields you send, so enabling a subscription does not reset its event filter. Setting active: true also clears the failure count.
Event catalogue
| Event | Meaning |
|---|---|
deposit_wallet_created | A deposit address was generated |
source_token_deposited | Funds arrived. Carries the deposit transaction hash |
rate_locked | The exchange rate was fixed |
vault_locking | Source funds are being locked into the vault |
vault_locked | Locked. Carries the gather transaction hash |
payout_processing | The payout was submitted |
target_token_sent | Confirmed. Carries the payout transaction hash |
swap_failed | Could not complete. Carries an error message |
swap_expired | The deposit window closed without a deposit |
simpleswap_* | SimpleSwap leg state changes |
test_ping | Sent only by the test endpoint |
Payload
json
{
"event": "target_token_sent",
"timestamp": "2026-09-18T18:43:31.743Z",
"swap_id": "7e0da07a-78e5-465d-a905-030c76355174",
"tx_hash": "0x3341c2aff0d8409c62f728ea8aef00f77cec5b1c32f91257ee930034876cc075",
"status": "completed",
"amount_sent": "63.34749608484891"
}Headers
| Header | Meaning |
|---|---|
X-Bridge-Event | The event type |
X-Bridge-Signature | HMAC-SHA256 of the raw body, hex |
X-Bridge-Timestamp | When the attempt was made |
X-Bridge-Delivery | Delivery ID, stable across retries |
X-Bridge-Subscription | The subscription it was sent to |
Verifying the signature
Hash the raw bytes
Re-serialising the parsed JSON can produce different bytes and will not match. Capture the body as raw bytes before any JSON middleware touches it.
js
const crypto = require('crypto');
app.post('/knyx-hooks', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto.createHmac('sha256', process.env.KNYX_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const presented = Buffer.from(req.headers['x-bridge-signature'] || '', 'utf8');
const computed = Buffer.from(expected, 'utf8');
if (presented.length !== computed.length ||
!crypto.timingSafeEqual(presented, computed)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Acknowledge first, work afterwards.
res.json({ ok: true });
process.nextTick(() => handleEvent(JSON.parse(req.body.toString('utf8'))));
});timingSafeEqual rather than ===: comparing strings leaks how long a shared prefix is, which is enough to forge a signature given time.
Retries
A destination that does not return 2xx within 10 seconds is retried automatically:
| Setting | Value |
|---|---|
| First retry | 30 seconds |
| Backoff | Doubling, with jitter |
| Cap | 6 hours |
| Maximum attempts | 8 |
After that the delivery stays in the history as undelivered and is not rescheduled. POST /api/webhook/retry/:eventId re-drives it by hand.
Every attempt is recorded with its HTTP status, error, attempt count and next scheduled time, readable at GET /api/webhook/subscriptions/:id/deliveries?failed=true.
What your handler must do
Acknowledge quickly. Return 2xx as soon as the payload is safely accepted, then do the work asynchronously. A handler slower than the timeout is recorded as a failure and the event is sent again — so slow processing turns into duplicate processing.
Be idempotent. Delivery is at-least-once. Deduplicate on X-Bridge-Delivery, or on swap_id plus event.
Testing before it matters
bash
curl -X POST https://bridge.testnet.kriptonyx.com/api/webhook/subscriptions/<id>/test \
-H "Authorization: Bearer $BRIDGE_ADMIN_TOKEN"json
{ "delivered": true, "status": 200, "error": null,
"event_id": "…", "delivery_id": "…" }If delivered is false, the status and error tell you why — and the attempt appears in the delivery history like any other.