If your integration already creates payment links or invoices and fulfils on the payment.succeeded webhook, it takes stablecoins as it is: the customer picks Pay with stablecoin on the same checkout, sends USDC or USDT from their wallet straight to your business wallet, and you get the same event once the transfer is final. This guide adds the on-chain details to your orders. How matching, confirmations and statuses work is on Stablecoin checkout.
1. Switch it on
- Set up your business wallet on the Wallets page.
- In Settings → Stablecoins (owners and admins), turn on Accept stablecoins, pick tokens and networks, and confirm the receiving address for each.
- Your links and invoices now offer Pay with stablecoin below the card form. Nothing changes in your code.
2. Fulfil on payment.succeeded
A stablecoin payment's payment.succeeded has paymentMethod.type stablecoin, an id starting pay_sc, and a stablecoin object with the chain, token, amounts, wallets and each transaction's explorer link. The stablecoin object comes with the webhook only: GET /api/v1/payments/{id} returns the payment without it, so store what you need when the event arrives.
// webhook.mjs — Node.js 18+ with Express: npm install express
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.ANYTIME_WEBHOOK_SECRET; // whsec_…
function verify(rawBody, header) {
const parts = Object.fromEntries(String(header ?? "").split(",").map((part) => part.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return (
typeof parts.v1 === "string" &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300
);
}
app.post("/webhooks/anytime", express.raw({ type: "application/json" }), async (req, res) => {
const rawBody = req.body.toString("utf8");
if (!verify(rawBody, req.get("Anytime-Signature"))) return res.status(400).send("Invalid signature");
const event = JSON.parse(rawBody);
// Skip event.id if you've handled it before.
if (event.type === "payment.succeeded") {
const payment = event.data;
const order = { id: payment.metadata.orderId, paymentId: payment.id, paidCents: payment.amountCents };
if (payment.paymentMethod?.type === "stablecoin" && payment.stablecoin) {
// Final on-chain: keep the proof with the order.
order.method = payment.paymentMethod.label; // "USDC on Ethereum"
order.txHash = payment.stablecoin.txHash;
order.explorerUrl = payment.stablecoin.explorerUrl;
order.tokenAmount = payment.stablecoin.amount; // what arrived, e.g. "25.004217"
}
await markPaid(order); // your database
}
res.sendStatus(200);
});
async function markPaid(order) {
console.log("paid", order);
}
app.listen(3000);3. Refunds and mistakes
- Refunds are manual: the tokens went to your own wallet, so send them back from it, in the same token on the same network. The refunds API doesn't refund stablecoin payments.
- An overpayment counts as paid and you're told the difference to send back; an expired or underpaid one tells you what arrived. Both show in the dashboard, not as webhooks.
- Only
payment.succeededis sent: there's no event for a stablecoin payment that never arrives, so let unpaid orders time out on your side as you would for an abandoned card checkout.