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.