Skip to content

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_TOKEN
bash
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

MethodPathPurpose
POST/api/webhook/subscriptionsRegister an endpoint
GET/api/webhook/subscriptionsList. Secrets are never returned
GET/api/webhook/subscriptions/:idOne subscription, with failure counters
PATCH/api/webhook/subscriptions/:idChange URL, events, method, description or active state
DELETE/api/webhook/subscriptions/:idRemove it and its queued deliveries
POST/api/webhook/subscriptions/:id/rotate-secretIssue a new secret
POST/api/webhook/subscriptions/:id/testSend a signed test_ping
GET/api/webhook/subscriptions/:id/deliveriesDelivery history
GET/api/webhook/event-typesCatalogue. No authentication
POST/api/webhook/retry/:eventIdForce 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

EventMeaning
deposit_wallet_createdA deposit address was generated
source_token_depositedFunds arrived. Carries the deposit transaction hash
rate_lockedThe exchange rate was fixed
vault_lockingSource funds are being locked into the vault
vault_lockedLocked. Carries the gather transaction hash
payout_processingThe payout was submitted
target_token_sentConfirmed. Carries the payout transaction hash
swap_failedCould not complete. Carries an error message
swap_expiredThe deposit window closed without a deposit
simpleswap_*SimpleSwap leg state changes
test_pingSent 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

HeaderMeaning
X-Bridge-EventThe event type
X-Bridge-SignatureHMAC-SHA256 of the raw body, hex
X-Bridge-TimestampWhen the attempt was made
X-Bridge-DeliveryDelivery ID, stable across retries
X-Bridge-SubscriptionThe 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:

SettingValue
First retry30 seconds
BackoffDoubling, with jitter
Cap6 hours
Maximum attempts8

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.

KriptoNyx testnet — chain ID 3009 (kriptonyx_3009-1)