Webhook Guide
Overview
Section titled “Overview”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.
Quick Start
Section titled “Quick Start”Register a Webhook
Section titled “Register a Webhook”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.
Event Types
Section titled “Event Types”| Event | Description |
|---|---|
payment_link.created | A new payment link was created |
order.created | Customer initiated a payment |
order.completed | Payment confirmed on-chain |
order.failed | Payment failed or expired |
transaction.confirmed | On-chain transaction confirmed |
Payload Format
Section titled “Payload Format”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 } }}Signature Verification
Section titled “Signature Verification”Every webhook request includes these headers:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature (hex) |
X-Webhook-Timestamp | Unix timestamp in seconds |
X-Webhook-Id | Event ID for idempotency |
Algorithm: HMAC-SHA256(secret, "<timestamp>.<raw_body>")
SDK Integration
Section titled “SDK Integration”Installation
Section titled “Installation”TypeScript/JavaScript:
npm install unuspay-sdk# oryarn add unuspay-sdk# orpnpm add unuspay-sdk# orbun add unuspay-sdkPython:
pip install unuspay-sdk fastapi# or with uvuv add unuspay-sdk fastapi# or with poetrypoetry add unuspay-sdk fastapiTypeScript Example
Section titled “TypeScript Example”The TypeScript SDK provides a Webhook class for signature verification with Zod validation and runtime detection (Node.js/Bun).
Express.js
Section titled “Express.js”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 verificationapp.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'.
Python Example
Section titled “Python Example”The Python SDK provides a Webhook class for signature verification with Pydantic validation.
FastAPI
Section titled “FastAPI”from fastapi import FastAPI, Request, HTTPException, BackgroundTasksfrom unuspay_sdk import Webhook, WebhookVerificationErrorimport 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 hereImportant: You must use the raw request body (before JSON parsing) for signature verification. In FastAPI, use await request.body().
SDK Reference
Section titled “SDK Reference”Both SDKs validate payload structure and signatures, providing a consistent API across TypeScript and Python.
Configuration Options
Section titled “Configuration Options”| Option | TypeScript | Python | Default | Description |
|---|---|---|---|---|
| Max Age | maxAgeSeconds | max_age_seconds | 300 | Maximum age of timestamp in seconds |
// TypeScriptconst webhook = new Webhook(secret, { maxAgeSeconds: 600 })# Pythonfrom unuspay_sdk import WebhookConfigwebhook = Webhook(secret=secret, config=WebhookConfig(max_age_seconds=600))Error Handling
Section titled “Error Handling”Both SDKs throw WebhookVerificationError with specific error codes:
| Error Code | Description | HTTP Status |
|---|---|---|
MISSING_HEADERS | Missing X-Webhook-Signature or X-Webhook-Timestamp | 400 |
INVALID_SIGNATURE | Signature does not match computed HMAC | 400 |
TIMESTAMP_EXPIRED | Timestamp is older than maxAgeSeconds | 400 |
INVALID_PAYLOAD | Payload is not valid JSON or missing required fields | 400 |
// TypeScriptimport { 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) }}# Pythonfrom 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', 400Replay Protection
Section titled “Replay Protection”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.
Runtime Detection
Section titled “Runtime Detection”The TypeScript SDK automatically detects the runtime environment (Bun, Node.js, or other) and uses the optimal crypto implementation transparently.
Operations
Section titled “Operations”Retry Behavior
Section titled “Retry Behavior”Failed deliveries (non-2xx response or timeout) retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 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.
Testing Webhooks
Section titled “Testing Webhooks”Use the test endpoint to send a sample webhook to your URL:
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" } }}Best Practices
Section titled “Best Practices”- Respond quickly — Return
200immediately, process the event asynchronously - Handle duplicates — Use the event
idfor idempotency; you may receive the same event multiple times - Log raw payloads — Store the raw request body before parsing to help debug signature issues
- Always verify signatures — Never trust unverified webhook payloads in production
- Use HTTPS — Webhook URLs must use HTTPS to protect payload contents