Orqpay

Webhooks

Get notified when a payment is paid — and verify each delivery.

Orqpay calls your HTTPS endpoint when a payment changes. Use that signal to fulfill orders without polling.

You can configure multiple endpoints per environment (test and live), each with its own signing secret and event groups.

Set up

In the merchant dashboard → Webhooks:

  1. Add an endpoint URL (and optional name)
  2. Choose event groups (new endpoints default to Fulfillment)
  3. On first save Orqpay mints an orq_whsec_… signing secret (or set your own opaque secret, 16–255 characters)
ModeURL
Testhttp or https (localhost and tunnels are fine)
LivePublic https only

Up to 10 endpoints per environment. API keys cannot change webhook settings — use the dashboard.

Event groups

GroupTypical use
Fulfillmentinvoice.paid, invoice.overpaid — ship / credit
Payments lifecycleOther invoice.* status events
Money / Railsdeposit.*, funding.credited
Treasuryfloat.*, withdrawal.*, admin funding-drain (cashout.*)
AllEvery event Orqpay emits (default for endpoints created before multi-endpoint)

Split fulfillment and ledger traffic across different URLs when you need to.

When to fulfill

EventWhat it meansWhat you should do
invoice.createdPayment createdOptional logging
invoice.underpaidPartial paymentWait
invoice.paidPaid on timeFulfill / ship
invoice.overpaidPaid above the amountFulfill; handle surplus if needed
invoice.expiredNo payment in timeClose the order
invoice.expired_underpaidStill short when the window endedClose or contact the buyer
invoice.settledSettlement finishedAccounting confirmation

Rule of thumb: ship only on invoice.paid.

Use data.paymentId as the payment id. Event type names use the invoice.* prefix for the commercial layer.

Migrated endpoints that listen to All may also receive Rails money events (deposit.credited, withdrawals, float). Prefer event groups so you only get what you need.

Example payload

{
  "id": "<paymentId>:<sequence>:invoice.paid",
  "type": "invoice.paid",
  "createdAt": "2026-07-04T12:00:00Z",
  "environment": "test",
  "data": {
    "paymentId": "uuid",
    "invoiceId": "uuid",
    "status": "PAID",
    "merchantId": "uuid",
    "referenceId": "your-order-ref",
    "amount": "10.00",
    "amountReceived": "10.00",
    "token": "USDC",
    "sequence": 3
  }
}
  • Amounts are decimal strings in major units ("10.00" = 10 USDC/USDT)
  • Handle each delivery once using envelope id (retries keep the same id)

Verify the signature

X-Orqpay-Signature: t=<unix_seconds>,v1=<hmac_hex>
  1. Parse t and v1
  2. Reject if the timestamp is older than 5 minutes
  3. HMAC-SHA256 {timestamp}.{rawBody} with your webhook secret (orq_whsec_… when platform-minted)
  4. Compare with constant-time equality
  5. Always verify the raw 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;
  }
}

Return 2xx quickly after you verify. Do heavy work asynchronously.

Delivery

  • Failed deliveries are retried with backoff
  • Inspect and resend from the dashboard (per endpoint)
  • Prefer webhooks over polling — see Payment status

Test vs live

Test keys and test endpoints only cover test payments. After go-live, use a live key and separate live webhook endpoints.

See also: Payment status · Go live · Errors

On this page