Webhooks
PrimCard pushes signed events to your HTTPS endpoint the moment something happens — no polling.
Events
card.issued/card.issue_failed— issuance finished (covers async 202 cases).topup.succeeded/topup.failed— a top-up settled or fell through.card.transaction.created— the cardholder purchased, was declined, or got a refund.card.frozen/card.unfrozen/card.blocked— status changes.deposit.confirmed— your USDT funding arrived and was credited.
Payload
POST https://your-server.example.com/webhooks/primcard
X-PrimCard-Event-Id: evt_63a6e28dca68…
X-PrimCard-Timestamp: 1786893456789
X-PrimCard-Signature: 3f1a9c… ← sha256 hex, see below
{
"id": "evt_63a6e28dca68…",
"type": "card.issued",
"created": "2026-08-12T09:15:00.000Z",
"mode": "live",
"data": { "card": { "id": "…", "status": "active", "balance": 10000, … } }
}Verify the signature — always
Every delivery is signed with your webhook secret (whsec_…, issued when webhooks are set up). Recompute over the raw request body — before any JSON parsing — and compare in constant time:
Node.js / Express
import { createHash, timingSafeEqual } from "node:crypto"
app.post("/webhooks/primcard", express.raw({ type: "*/*" }), (req, res) => {
const ts = req.header("X-PrimCard-Timestamp") ?? ""
const sig = req.header("X-PrimCard-Signature") ?? ""
const expected = createHash("sha256")
.update(ts + req.body.toString("utf8") + process.env.PRIMCARD_WEBHOOK_SECRET)
.digest("hex")
const valid =
sig.length === expected.length &&
timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
if (!valid) return res.status(401).end()
const event = JSON.parse(req.body)
// handle event.type — but do it AFTER responding (see below)
res.status(200).end()
})Delivery rules
- Respond 2xx within 10 seconds. Acknowledge first, process after — a slow handler reads as a failure.
- Failures retry automatically: 30 seconds, then 5 minutes, 30 minutes, and 2 hours before we give up (PrimCard can requeue given-up events manually).
- Dedupe on
id— retries reuse the same event id, so the same event can arrive more than once. Processing must be idempotent. - Ordering is not guaranteed under retries — trust each event's payload, or re-fetch the resource (
GET /cards/{id}) when in doubt.
Test it end to end before going live: point your endpoint at a tunnel (e.g.
ngrok), create a test card, then fire POST /test/cards/{id}/simulate-transaction and watch card.transaction.created arrive signed.