Accept payments

Webhooks

We send a signed HTTPS POST to your server every time an invoice changes status. Webhooks are the source of truth for granting access: verify the signature, store the event ID, answer 2xx.

Setup

Send us one HTTPS endpoint. We give you a secret whsec_…; it is shown once. Test and live events go to the same endpoint: check event.mode and ignore "test" events in production if you share the endpoint between environments. The header may carry more than one v1 signature; any matching one is valid.

Events

EventMeaningWhat to do
invoice.payment_detectedA transfer to the address was found and is waiting for confirmations. Sent once per counted transfer.Show "payment received, confirming". Do not grant access yet.
invoice.paidPaid in full (within the tolerance). With resolution: "manual" our operator confirmed it by hand.Grant access.
invoice.paid_overpaidReceived more than the amount plus tolerance. Can follow invoice.paid when a second transfer arrives.Grant access; credit received_usd_cents − amount_usd_cents if your policy says so.
invoice.paid_latePaid in full, but the transfer was sent after the invoice expired.Grant access (or refund, by your policy).
invoice.partially_paidLess than the amount minus tolerance. The customer can send the rest to the same address within 24 hours after expiry.Wait. Do not grant access.
invoice.needs_reviewA case we do not decide automatically: a late payment that is not enough, a different coin sent to the address, a native-coin price we could not confirm.Wait for our operator or your support to resolve it; a paid or cancelled event follows.
invoice.expiredNo payment within the lifetime.Mark the order unpaid. A transfer after this point arrives as paid_late or goes to review.
invoice.cancelledClosed: cancel_reason is merchant (your API call), operator, or network_changed.If replaced_by is set, the customer switched network: the order continues on the new invoice.

Payload

The body is { id, type, created, mode, data: { invoice } }, where invoice is the full invoice object, including your metadata. created is a Unix timestamp in seconds.

HTTP
POST /webhooks/axiomispay HTTP/1.1
Content-Type: application/json
User-Agent: AxiomisPay/0.1.0
AxiomisPay-Event-Id: evt_3kP9…
AxiomisPay-Signature: t=1760260792,v1=5f0c2a…9be1

{
  "id": "evt_3kP9…",
  "type": "invoice.paid",
  "created": 1760260792,
  "mode": "live",
  "data": {
    "invoice": {
      "id": "inv_7Qx2mK9aLp3RtV8sNc4WbY1z",
      "status": "paid",
      "amount_usd_cents": 2900,
      "received_usd_cents": 2900,
      "external_order_id": "1042",
      "customer_ref": "user-77",
      "metadata": { "plan": "pro" },
      "network": "tron",
      "asset": "tron:usdt",
      "resolution": "auto",
      "transfers": [ … ]
    }
  }
}

Verify the signature

AxiomisPay-Signature: t=<unix seconds>,v1=<hex>, where v1 = HMAC-SHA256(secret, t + "." + raw_body). Reject the request if the timestamp is more than 5 minutes off or no v1 matches. Compute the HMAC over the raw bytes of the body: parsing and re-serializing JSON changes them and breaks the signature.

import express from 'express';
import { verifyWebhook, WebhookVerificationError } from '@axiomis-pay/sdk';

app.post('/webhooks/axiomispay', express.raw({ type: '*/*' }), async (req, res) => {
  let event;
  try {
    event = verifyWebhook(req.body, req.headers['axiomispay-signature'], process.env.AXIOMISPAY_WEBHOOK_SECRET);
  } catch (e) {
    if (e instanceof WebhookVerificationError) return res.status(400).send(e.code);
    throw e;
  }
  // Store event.id first: a duplicate delivery must not grant access twice.
  if (await db.webhookSeen(event.id)) return res.sendStatus(200);
  await handle(event);
  res.sendStatus(200);
});

Delivery and retries

  • Any 2xx within 10 seconds counts as delivered. Redirects are not followed. Answer first, do slow work in a queue.
  • Otherwise we retry after 30 s, 1, 2, 5, 10 and 30 minutes, then 1, 2, 4 and 6 hours, for up to 24 hours. After that the event is marked failed and our operator is alerted; it can be resent from the console.
  • event.id stays the same across retries. Events can arrive more than once and, rarely, out of order: rely on data.invoice.status, and when in doubt fetch GET /v1/invoices/:id.

Make the handler idempotent

Write event.id to a table with a unique constraint in the same transaction that grants access. A duplicate delivery then fails the insert and changes nothing.

Test the handler without a payment: signWebhook(secret, body) from the SDK builds a valid header for your own tests. See SDK.

Webhooks · Axiomis Pay Docs