Skip to content

Webhooks

A webhook is an HTTP request Invoise sends to your server when a payment changes. You can start with GET status checks and add webhooks when you need automatic updates.

Terminal window
curl -X POST 'https://platform.invoise.me/api/v1/shops/{shop_id}/webhooks' \
-H 'Authorization: Bearer ivk_...' \
-H 'Idempotency-Key: <saved-unique-key>' \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/invoise","filters":["transfer","invoice","payout"]}'

Use your own public HTTPS URL. Local and private addresses are rejected. Save the secret returned when the endpoint is created: you need it to verify requests.

A shop can have up to 10 endpoints. An endpoint’s URL can be changed, so reuse one instead of adding another. See Limits.

Event Meaning
transfer.observed A transfer was seen; it is not confirmed yet.
transfer.confirmed An incoming transfer has enough confirmations. It may be only a partial invoice payment.
transfer.reverted A previous transfer event was reversed.
invoice.closed The invoice closed on chain.
invoice.cancelled The checkout was cancelled or the invoice expired; this is not a refund.
payout.sent The payout was broadcast or its outgoing transfer was seen.
payout.confirmed Complete settlement accounting is available.
payout.reverted A previous payout confirmation was reversed.
payout.failed A terminal payout failure needs intervention.

For invoice.cancelled, data.reason says why: expired when the invoice lifetime ran out, merchant_blocked when Invoise staff blocked the merchant. A cancellation by the merchant has no reason.

Subscribe by group: transfer, invoice, payout. Incoming transfers and outgoing payouts are different events. A temporary RPC or gas delay is not payout.failed.

gas.wait and sweep.failed are accepted as filters, but Invoise does not send them to your endpoint. They are internal signals: gas.wait means a payout is waiting for network fee funds and continues on its own; sweep.failed means an attempt to move the funds to your recipient failed. You need no action for either. A payout that cannot complete reaches you as payout.failed.

In a sandbox shop you can simulate both through POST /shops/{shop_id}/sandbox/simulate to check that a delayed payout does not break your flow. They change the payout’s state only and send no webhook.

Invoise-Signature has the form t=<timestamp>,v1=<hex>. The signature is HMAC-SHA256 of timestamp + "." + raw_body, using your webhook secret.

The following Node.js example also rejects timestamps more than five minutes from the server clock. Keep that clock synchronised and choose your tolerance deliberately.

import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret) {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? '');
if (!match) return false;
const [, timestamp, signature] = match;
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const expected = createHmac('sha256', secret)
.update(timestamp + '.')
.update(rawBody)
.digest();
return timingSafeEqual(Buffer.from(signature, 'hex'), expected);
}

Pass the original request bytes as rawBody. Do not parse JSON and stringify it before checking the signature. Reject invalid signatures before storing or acting on their contents.

A shortened payload looks like this:

{
"id": "<event-id>",
"type": "transfer.confirmed",
"created_at": "2026-09-19T10:00:00Z",
"data": {
"issuance_id": "<issuance-id>",
"shop_id": "<shop-id>",
"external_id": "order-123",
"type": "invoice",
"invoice_cancelled": false
}
}
Case Handling
Duplicates Deduplicate using Invoise-Event-ID, which matches the envelope’s id. Save the ID and the business update atomically.
Accepting an event Save accepted events durably, return 2xx promptly, and process queued work afterwards.
Retries and replays Retries and manual replays repeat the event ID. Invoise-Attempt-ID changes and is not a deduplication key.
Event order Events can arrive out of order. Use their contents and reconcile against current status when needed.

For reversals, chain_event.reference_event_id points to the earlier chain event. Payout payloads include a stable payout_id, transaction hash, amounts and fee details; unknown amounts are null. Their incoming-transfer ID list is capped at 100 and flags truncation.

Older stored events may use project_id; read shop_id first and fall back to it if needed. Replays keep the original bytes.

List GET /api/v1/shops/{shop_id}/deliveries. Replay a delivery with POST /api/v1/shops/{shop_id}/deliveries/{id}/replay and a saved idempotency key. Replaying does not create a new business event.