Webhooks
Receive signed callbacks for invoice status changes and verify them safely.
Orqpay sends an HTTP POST to your webhook URL when an invoice changes state. Configure one endpoint (and secret) per environment — test and live are separate.
Configure
Set the webhook URL and signing secret in the merchant dashboard (Webhooks).
Merchant API keys cannot configure webhooks — use the dashboard.
| Environment | URL rules |
|---|---|
| Test | http or https (localhost / tunnels allowed) |
| Live | Public https only |
Secret is required on first setup (16–255 characters). Leave it blank on later updates to keep the existing secret.
Events
| Event | Meaning | What you should do |
|---|---|---|
invoice.created | Invoice created | Optional logging |
invoice.payment_detected | Payment seen, not confirmed yet | Wait |
invoice.underpaid | Partial payment | Wait for top-up |
invoice.paid | Paid on time | Fulfill / ship |
invoice.late_paid | Paid after checkout expired | Funds received — do not auto-fulfill |
invoice.overpaid | Amount above required | Contact support if you need help |
invoice.expired | No payment in time | Close the order |
invoice.expired_underpaid | Top-up window ended still short | Close / contact customer |
invoice.settled | Settlement finished | Accounting confirmation |
invoice.payment_invalidated | A previous payment was reversed on-chain | Reconcile; do not fulfill on detection alone |
Fulfillment rule: treat only invoice.paid as safe to ship. Use invoice.settled for accounting. Never treat invoice.late_paid as a normal fulfillment signal.
Payload
{
"id": "<invoiceId>-<sequence>",
"type": "invoice.paid",
"createdAt": "2026-07-04T12:00:00Z",
"environment": "test",
"data": {
"invoiceId": "uuid",
"status": "PAID",
"merchantId": "uuid",
"referenceId": "your-order-ref",
"metadata": {},
"amount": "10",
"amountReceived": "10",
"token": "USDC",
"sequence": 3
}
}amount/amountReceivedare decimal strings in major units ("10"= 10 USDC/USDT).- Process each delivery idempotently using envelope
id(retries reuse the sameid).
Verify signatures
Header:
X-Coinpay-Signature: t=<unix_seconds>,v1=<hmac_hex>- Parse
tandv1 - Reject if the timestamp is older than 5 minutes
- Compute HMAC-SHA256 of
{timestamp}.{rawBody}with your webhook secret - Compare with constant-time equality
- Always verify the raw request body (do not re-serialize JSON)
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyOrqpaySignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=")),
);
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) return false;
const ageSec = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(ageSec) || ageSec > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
try {
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
} catch {
return false;
}
}Respond with 2xx quickly after verifying. Do heavy work asynchronously on your side.
Delivery
- Orqpay retries failed deliveries with backoff
- Inspect and resend from the merchant dashboard (Webhooks)
- Prefer webhooks over polling; see Invoice lifecycle
Test vs live
Test API keys only affect test invoices and the test webhook. Live uses a separate URL and secret after go-live.
See also: Invoice lifecycle · Errors