Webhooks
If webhookUrl is provided during invoice creation, the API will send a webhook when payment is detected for the first time (pending -> paid).
Delivery Behavior
- Trigger: New payment detection (
status = paid). - Method:
POST. - Content-Type:
application/json. - Queue type: Local in-memory queue.
- Retries: Up to 20 attempts with 1 minute delay between attempts.
- Success criteria: Target endpoint returns any
2xxstatus.
Webhook Auth Header
Webhook requests include the same authentication type used when the invoice was created:
- If invoice was created with API Key:
x-depay-api-key: <OWNER_API_KEY>
- If invoice was created with Request Signature:
x-depay-signature: <ORIGINAL_REQUEST_SIGNATURE>
Webhook Payload Example
{
"ownerAddress": "0x501BEF961A6f40E063efD6048768b0BC35ab1428",
"chainId": 8453,
"invoiceId": "1000456",
"paymentAddress": "0x6c76fD5f0b5F7924A0b6b23223A8D2398fB00E72",
"amount": "12.5",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"status": "paid",
"paid": true,
"paidAt": 1735600000000,
"requiredRaw": "12500000",
"balanceRaw": "12500000",
"balance": 12.5,
"updatedAt": 1735600001000
}
Receiver Example
Compare the incoming auth header against your own credential, ack with 2xx
before doing slow work, and treat delivery as at-least-once — the same
pending -> paid event can arrive more than once after a retry.
- TypeScript
- Python
- cURL
webhook-route.ts
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.json());
interface InvoicePaidEvent {
ownerAddress: string;
chainId: number;
invoiceId: string;
paymentAddress: string;
amount: string;
tokenAddress: string | null;
status: "paid";
paid: boolean;
paidAt: number;
requiredRaw: string;
balanceRaw: string;
balance: number;
updatedAt: number;
}
function timingSafeEqual(a: string, b: string): boolean {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
return bufA.length === bufB.length && crypto.timingSafeEqual(bufA, bufB);
}
app.post("/webhooks/invoice", async (req, res) => {
const apiKey = req.header("x-depay-api-key") ?? "";
if (!timingSafeEqual(apiKey, process.env.SODAPOP_API_KEY ?? "")) {
return res.status(401).json({ error: "unauthorized" });
}
const event = req.body as InvoicePaidEvent;
// Ack fast — the queue retries for an hour if we time out.
res.status(200).json({ received: true });
// Delivery is at-least-once, so keep the handler idempotent.
await markOrderPaid(event.invoiceId, {
chainId: event.chainId,
paymentAddress: event.paymentAddress,
amount: event.amount,
paidAt: event.paidAt,
});
});
app.listen(3000);
webhook_route.py
import hmac
import os
from fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Request
app = FastAPI()
API_KEY = os.environ["SODAPOP_API_KEY"]
@app.post("/webhooks/invoice")
async def invoice_webhook(
request: Request,
background_tasks: BackgroundTasks,
x_depay_api_key: str = Header(default=""),
):
if not hmac.compare_digest(x_depay_api_key, API_KEY):
raise HTTPException(status_code=401, detail="unauthorized")
event = await request.json()
# Ack fast — the queue retries for an hour if we time out.
# Delivery is at-least-once, so keep the handler idempotent.
background_tasks.add_task(
mark_order_paid,
event["invoiceId"],
chain_id=event["chainId"],
payment_address=event["paymentAddress"],
amount=event["amount"],
paid_at=event["paidAt"],
)
return {"received": True}
Replay a delivery against your own endpoint to test the handler locally:
curl -X POST 'https://merchant.example.com/webhooks/invoice' \
-H 'Content-Type: application/json' \
-H "x-depay-api-key: $SODAPOP_API_KEY" \
-d '{
"ownerAddress": "'"$OWNER_ADDRESS"'",
"chainId": 8453,
"invoiceId": "1000456",
"paymentAddress": "0x6c76fD5f0b5F7924A0b6b23223A8D2398fB00E72",
"amount": "12.5",
"tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"status": "paid",
"paid": true,
"paidAt": 1735600000000
}'
:::caution paid is not completed
The webhook fires on payment detection, not on settlement. If your business logic
needs funds withdrawn to the owner wallet, poll
GET /integration/invoices/status
until status is completed.
:::
Important Notes
- Webhook queue is not persisted in the database (minimal mode).
- Pending webhook attempts are lost on process restart.
- The
ownerAddressremains required in all invoice API requests regardless of the authentication method. See Auth Model for details.