Webhooks
Receive push notifications when trade opportunity state changes.
- Configure webhook URL per tenant.
- Delivery uses durable enqueueing when available and retries failed deliveries.
- Use the one-time Webhook Signing Secret returned by the rotation endpoint to verify the
X-SWAPS-Signatureheader or thesignaturefield in the JSON body. - Event headers include
X-SWAPS-Event,X-SWAPS-Tenant, andX-SWAPS-Timestamp.
Signature Verification
SWAPS computes a lowercase hexadecimal HMAC-SHA256 using these exact inputs:
- Build the outbound payload without its
signatureproperty. - Serialize that object with JavaScript
JSON.stringify. - Compute
HMAC-SHA256(webhookSecret, serializedPayload)and encode the digest as lowercase hex. - Add the resulting signature to the JSON body and the
X-SWAPS-Signatureheader.
The signature does not cover the final raw request body including its signature property. Verify against the parsed payload with that property removed. In Node.js:
import crypto from 'node:crypto';
export function verifySwapsWebhook(body, signatureHeader, webhookSecret) {
const { signature: bodySignature, ...unsignedPayload } = body;
const supplied = signatureHeader || bodySignature;
if (typeof supplied !== 'string' || !/^[0-9a-f]{64}$/i.test(supplied)) return false;
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(unsignedPayload), 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(supplied, 'hex'),
Buffer.from(expected, 'hex'),
);
}
Delivery and Retries
Any HTTP 2xx response is successful. The direct-delivery fallback allows three total attempts: the initial request, a retry after approximately 1 second, and a final retry after approximately 5 seconds. Each request has a 10-second timeout. Network errors, timeouts, and non-2xx responses enter the same retry path. Durable outbox delivery can redeliver independently, so receivers must be idempotent.
The public webhook payload currently has no stable delivery ID. Build a receiver-side deduplication key from the event name, payload timestamp, trade-loop ID, and transaction hash where present. This is an integration convention, not a server-issued delivery identifier.
Signing-secret lifecycle
Tenant profile and settings responses never include the webhook signing secret. Replace it only from a trusted server-key control-plane context:
curl -X POST https://www.swapsapi.com/api/v2/tenant/webhooks/secret/rotate \
-H "Authorization: Bearer sk_live_your_server_key"
The response returns the new 64-character hexadecimal secret exactly once. Rotation takes effect immediately and the previous secret stops verifying before you can install the new value, so plan a short verification-maintenance window. Open your receiver's secret manager first, rotate, then install the new secret immediately. Do not place the secret in browser storage, URLs, application logs, or ordinary tenant settings updates.
Current event names:
trade_loop_discoveredtrade_loop_invalidatedtrade_loop_completedswap_failed
Example payload:
{
"event": "trade_loop_discovered",
"timestamp": "2026-05-05T10:35:00Z",
"tenant": {
"id": "tenant_123",
"name": "Example Collectibles"
},
"data": {
"loop": {
"id": "trade_123",
"status": "discovered",
"totalParticipants": 3,
"estimatedValue": 42,
"createdAt": "2026-05-05T10:35:00.000Z",
"blockchain": "ethereum",
"steps": [
{
"from": "0x1111111111111111111111111111111111111111",
"to": "0x2222222222222222222222222222222222222222",
"nfts": [
{
"address": "0x3333333333333333333333333333333333333333:1",
"name": "Charizard PSA 10",
"image": "https://cdn.example/charizard.png",
"collection": "Pokemon Base Set",
"floorPrice": 500,
"currency": "USD"
}
]
}
]
},
"trigger": "want_submitted",
"metadata": {
"source": "api"
}
},
"signature": "hmac_hex"
}
Webhook payloads are sanitized before delivery. They include public trade state and asset metadata, not internal scoring inputs or private route diagnostics.