Webhooks
If you pass webhookUrl when creating an invoice,
SodaPop sends a POST to it when a payment is first detected (pending → paid,
or expired → paid for a late payment).
Payload
{
"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
}
tokenAddressis absent for native-token invoices.balancecan be higher thanamountif the payer overpaid.
Authenticating webhooks
The webhook carries the same credential you used to create the invoice:
| Invoice created with | Header | How to verify |
|---|---|---|
| API key | x-depay-api-key: <your API key> | Constant-time compare with your key. |
| Request signature | x-depay-signature: <signature of the create request> | Compare with the signature you stored when creating the invoice. It is not a signature of the webhook body. |
Delivery
| Success | Any 2xx response |
| Retries | Every 60 seconds, up to 20 attempts |
| Guarantees | At-least-once; delivery is not guaranteed |
Webhooks are a fast path, not the source of truth. Reconcile open orders with
GET /integration/invoices/status.
Receiver example
Verify, respond 2xx quickly, then process idempotently.
- TypeScript
- Python
- cURL
webhook-route.ts
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.json());
const API_KEY = process.env.SODAPOP_API_KEY ?? "";
function safeEqual(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/sodapop", async (req, res) => {
if (!safeEqual(req.header("x-depay-api-key") ?? "", API_KEY)) {
return res.status(401).json({ error: "unauthorized" });
}
const event = req.body;
res.status(200).json({ received: true });
// Delivery is at-least-once: key your handler on paymentAddress.
await markOrderPaid({
invoiceId: event.invoiceId,
paymentAddress: event.paymentAddress,
chainId: event.chainId,
balance: event.balance,
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/sodapop")
async def sodapop_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()
# Respond fast; delivery is at-least-once, so keep the handler idempotent.
background_tasks.add_task(
mark_order_paid,
invoice_id=event["invoiceId"],
payment_address=event["paymentAddress"],
balance=event["balance"],
paid_at=event["paidAt"],
)
return {"received": True}
Send a test event to your own endpoint:
curl -X POST 'https://merchant.example.com/webhooks/sodapop' \
-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,
"balance": 12.5
}'
:::caution paid is not completed
The webhook fires when the payment is detected, before funds reach your payout
address. That is the right moment to fulfil the order. If your accounting needs
the settled state, poll the invoice until status is completed.
:::