Skip to main content

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
}
  • tokenAddress is absent for native-token invoices.
  • balance can be higher than amount if the payer overpaid.

Authenticating webhooks​

The webhook carries the same credential you used to create the invoice:

Invoice created withHeaderHow to verify
API keyx-depay-api-key: <your API key>Constant-time compare with your key.
Request signaturex-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​

SuccessAny 2xx response
RetriesEvery 60 seconds, up to 20 attempts
GuaranteesAt-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.

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);

:::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. :::