Skip to content

Webhook Guide

Webhooks notify your server in real-time when payment events occur. Your endpoint must be HTTPS and respond with a 2xx status within 30 seconds.

Terminal window
curl -X POST https://api.unuspay.com/api/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/unuspay",
"events": ["order.completed", "order.failed"]
}'

Response:

{
"status": "success",
"data": {
"webhook_id": "whk_abc123",
"url": "https://your-server.com/webhooks/unuspay",
"events": ["order.completed", "order.failed"],
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}

Important: The secret is shown only once. Store it securely for signature verification.

EventDescription
payment_link.createdA new payment link was created
order.createdCustomer initiated a payment
order.completedPayment confirmed on-chain
order.failedPayment failed or expired
transaction.confirmedOn-chain transaction confirmed

All webhook payloads follow this structure:

{
"id": "evt_abc123def456",
"type": "order.completed",
"created_at": "2026-03-04T12:35:12.456Z",
"data": {
"object": {
"order_id": "ord_xxx",
"link_id": "link_xxx",
"status": "completed",
"amount": "100.00",
"currency": "USD",
"from_address": "0x1234...abcd",
"from_chain_id": 137
}
}
}

Every webhook request includes these headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature (hex)
X-Webhook-TimestampUnix timestamp in seconds
X-Webhook-IdEvent ID for idempotency

Algorithm: HMAC-SHA256(secret, "<timestamp>.<raw_body>")

TypeScript/JavaScript:

Terminal window
npm install unuspay-sdk
# or
yarn add unuspay-sdk
# or
pnpm add unuspay-sdk
# or
bun add unuspay-sdk

Python:

Terminal window
pip install unuspay-sdk fastapi
# or with uv
uv add unuspay-sdk fastapi
# or with poetry
poetry add unuspay-sdk fastapi

The TypeScript SDK provides a Webhook class for signature verification with Zod validation and runtime detection (Node.js/Bun).

import express from 'express'
import { Webhook, WebhookVerificationError } from 'unuspay-sdk'
const app = express()
const webhook = new Webhook(process.env.WEBHOOK_SECRET!)
// Important: Use raw body parser for signature verification
app.post('/webhooks/unuspay', express.text({ type: 'application/json' }), async (req, res) => {
try {
const event = await webhook.verify(
req.body,
req.headers['x-webhook-signature'] as string,
req.headers['x-webhook-timestamp'] as string
)
// Process event asynchronously, respond immediately
console.log('Processing event:', event.id, event.type)
res.status(200).send('OK')
} catch (err) {
if (err instanceof WebhookVerificationError) {
return res.status(400).send('Invalid signature')
}
res.status(500).send('Internal error')
}
})

Important: You must use the raw request body (before JSON parsing) for signature verification. In Express, use express.text() or a middleware like body-parser with type: 'application/json' and limit: '1mb'.

The Python SDK provides a Webhook class for signature verification with Pydantic validation.

from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from unuspay_sdk import Webhook, WebhookVerificationError
import os
app = FastAPI()
webhook = Webhook(secret=os.environ['WEBHOOK_SECRET'])
@app.post('/webhooks/unuspay')
async def handle_webhook(request: Request, background_tasks: BackgroundTasks):
try:
payload = await request.body()
event = webhook.verify(
payload=payload,
signature=request.headers['X-Webhook-Signature'],
timestamp=request.headers['X-Webhook-Timestamp']
)
# Process asynchronously, respond immediately
background_tasks.add_task(process_event, event)
return {'status': 'ok'}
except WebhookVerificationError as e:
raise HTTPException(status_code=400, detail='Invalid signature')
except Exception as e:
raise HTTPException(status_code=500, detail='Internal error')
async def process_event(event: dict):
"""Process webhook event asynchronously"""
print(f"Processing event: {event['id']} - {event['type']}")
# Your event handling logic here

Important: You must use the raw request body (before JSON parsing) for signature verification. In FastAPI, use await request.body().

Both SDKs validate payload structure and signatures, providing a consistent API across TypeScript and Python.

OptionTypeScriptPythonDefaultDescription
Max AgemaxAgeSecondsmax_age_seconds300Maximum age of timestamp in seconds
// TypeScript
const webhook = new Webhook(secret, { maxAgeSeconds: 600 })
# Python
from unuspay_sdk import WebhookConfig
webhook = Webhook(secret=secret, config=WebhookConfig(max_age_seconds=600))

Both SDKs throw WebhookVerificationError with specific error codes:

Error CodeDescriptionHTTP Status
MISSING_HEADERSMissing X-Webhook-Signature or X-Webhook-Timestamp400
INVALID_SIGNATURESignature does not match computed HMAC400
TIMESTAMP_EXPIREDTimestamp is older than maxAgeSeconds400
INVALID_PAYLOADPayload is not valid JSON or missing required fields400
// TypeScript
import { Webhook, WebhookVerificationError } from 'unuspay-sdk'
try {
webhook.verify(payload, signature, timestamp)
} catch (err) {
if (err instanceof WebhookVerificationError) {
console.error('Error code:', err.code) // e.g., 'INVALID_SIGNATURE'
return res.status(400).send(err.message)
}
}
# Python
from unuspay_sdk import WebhookVerificationError
try:
webhook.verify(payload, signature, timestamp)
except WebhookVerificationError as e:
print(f'Error code: {e.code}') # e.g., 'INVALID_SIGNATURE'
return 'Invalid signature', 400

Webhook signatures include a timestamp to prevent replay attacks. The SDK automatically rejects requests where the timestamp is older than maxAgeSeconds (default: 300 seconds) or in the future. Customize this tolerance using the configuration options above.

The TypeScript SDK automatically detects the runtime environment (Bun, Node.js, or other) and uses the optimal crypto implementation transparently.

Failed deliveries (non-2xx response or timeout) retry with exponential backoff:

AttemptDelay
1Immediate
2~5 seconds
3~25 seconds
4~2 minutes
5~10 minutes
6~52 minutes
7~4.3 hours

After 7 failed attempts, events move to a dead letter queue. Contact support to inspect failed deliveries.

Use the test endpoint to send a sample webhook to your URL:

Terminal window
curl -X POST https://api.unuspay.com/api/v1/webhooks/whk_abc123/test \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"status": "success",
"data": {
"event_id": "evt_test_abc123",
"webhook_id": "whk_abc123",
"event_type": "test",
"status": "pending",
"attempts": 0,
"max_attempts": 1,
"created_at": "2026-03-04T12:35:12.456Z"
}
}

The test event will be delivered to your webhook URL with this payload:

{
"id": "evt_test_abc123",
"type": "test",
"created_at": "2026-03-04T12:35:12.456Z",
"data": {
"object": {
"message": "This is a test webhook event",
"webhook_id": "whk_abc123"
}
}
}
  1. Respond quickly — Return 200 immediately, process the event asynchronously
  2. Handle duplicates — Use the event id for idempotency; you may receive the same event multiple times
  3. Log raw payloads — Store the raw request body before parsing to help debug signature issues
  4. Always verify signatures — Never trust unverified webhook payloads in production
  5. Use HTTPS — Webhook URLs must use HTTPS to protect payload contents