Webhooks
Receive real-time notifications when events occur in your Sardis account. Signature verification, retry behavior, and event types.
Setup
Register a webhook endpoint to receive event notifications. Your endpoint must be a publicly accessible HTTPS URL.
curl -X POST https://api.sardis.sh/api/v2/webhooks \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/sardis-webhook",
"events": ["payment.completed", "payment.failed", "wallet.frozen"],
"secret": "<your-webhook-signing-secret>"
}'The secret is used to sign webhook payloads so you can verify they came from Sardis. Store it securely — it is only shown once at creation time.
Event Types
| Event | Description |
|---|---|
payment.completed | Payment executed and confirmed on-chain |
payment.failed | Payment failed (insufficient funds, chain error) |
payment.pending | Payment submitted, awaiting confirmation |
wallet.created | New wallet provisioned |
wallet.frozen | Wallet frozen due to policy or manual action |
wallet.unfrozen | Wallet unfrozen and active again |
policy.violated | Spending policy violation detected |
hold.created | Pre-authorization hold placed |
hold.captured | Hold captured (payment completed) |
hold.voided | Hold voided (cancelled) |
compliance.flagged | Transaction flagged by compliance screening |
Payload Schema
All webhook payloads follow this structure:
{
"event_id": "evt_abc123def456",
"type": "payment.completed",
"created_at": "2026-03-24T10:30:00Z",
"data": {
"payment_id": "pay_xyz789",
"wallet_id": "wallet_abc123",
"amount": "50.00",
"token": "USDC",
"chain": "base",
"tx_hash": "0xabcdef...",
"status": "success"
}
}The event_id is unique per delivery and can be used for idempotent processing. The data field varies by event type.
Signature Verification
Every webhook delivery includes an X-Sardis-Signature header containing an HMAC-SHA256 signature. Always verify signatures before processing events.
Python
import hashlib
import hmac
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
# In your webhook handler:
@app.post("/sardis-webhook")
async def handle_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Sardis-Signature", "")
if not verify_webhook(body, signature, WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
event = json.loads(body)
# Process event...
return {"ok": True}TypeScript
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(
payload: string,
signature: string,
secret: string
): boolean {
const expected =
"sha256=" + createHmac("sha256", secret).update(payload).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Retry Behavior
If your endpoint returns a non-2xx status code or times out, Sardis retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry (final) | 24 hours |
After 5 failed attempts, the delivery is marked as failed. You can view delivery history and manually retry from the dashboard or via the API.
Best Practices
- Always verify the
X-Sardis-Signatureheader before processing events - Use the
event_idfield for idempotent processing — you may receive the same event more than once - Return a 200 response quickly, then process the event asynchronously
- Store your webhook secret in an environment variable, never in code
- Use HTTPS endpoints only — HTTP is rejected in production