Skip to documentationDocumentation
On this page

Overview

Receive signed invoice, payout, and refund events with reliable deduplication.

Webhooks notify your server about invoice, payout, refund, and manual conversion facts. Register your HTTPS endpoint and its event subscriptions in the merchant dashboard under Developers → Webhooks & deliveries before accepting payments.

Overview#

A merchant can have up to 16 endpoints per environment. Each endpoint has its own secret and subscription list. Live endpoints receive live events; test endpoints receive test events. Endpoint administration is not part of the merchant API.

An event can be delivered more than once, and delivery order is not guaranteed. Retries are bounded: a permanently rejected or exhausted delivery may never reach your application. Use webhooks with resource GET requests and reconciliation, not as your only financial record.

Events that occurred before an endpoint subscribed are not available as a historical catch-up feed.

Payload structure#

json example
{
  "id": "00000000-0000-0000-0000-000000000001",
  "event_seq": 184203,
  "type": "refund.created",
  "schema_version": 1,
  "environment": "live",
  "merchant_id": "00000000-0000-0000-0000-000000000002",
  "subject_type": "refund",
  "subject_id": "00000000-0000-0000-0000-000000000003",
  "occurred_at": "2026-09-14T10:00:00Z",
  "data": {
    "refund_id": "00000000-0000-0000-0000-000000000003",
    "invoice_id": "00000000-0000-0000-0000-000000000004",
    "currency": "USDC",
    "network": "ethereum",
    "environment": "live",
    "amount": "5.00",
    "recipient_amount": "5.00",
    "merchant_debit_amount": "5.10",
    "destination": "0x0000000000000000000000000000000000000001",
    "status": "pending"
  }
}
Envelope fieldMeaning
idEvent UUID; stable across attempts and endpoints; use for deduplication
event_seqGlobal event insertion sequence; gaps and differences from commit/delivery order are possible
typeEvent name from the catalog
schema_versionVersion of that event's payload contract
environmentlive or test
merchant_idMerchant owning the event
subject_typeResource type: invoice, payout, refund, or manual_conversion for catalog events
subject_idResource UUID
occurred_atTime of the business fact, not the HTTP delivery attempt
dataFields specific to the event type and schema version

The envelope bytes remain unchanged between retries. The delivery attempt receives a new signature timestamp.

Delivery headers#

HeaderValue
Content-Typeapplication/json
User-Agenthalfin-webhooks/1
X-Halfin-Event-IdSame UUID as the body's id
X-Halfin-Delivery-IdDelivery identifier for the event and endpoint
X-Halfin-Signaturet=<unix-seconds>,v1=<hex HMAC-SHA256>

Retries of the same delivery can retain the delivery ID. Two endpoints receive different delivery IDs for the same event ID. Always deduplicate by event ID, not the delivery ID or signature.

Delivery guarantees#

Return 2xx only after durably storing the verified event. Queue business processing and respond within 15 seconds. Your response body is not used to decide success; keep it free of secrets because a short response excerpt may appear in delivery logs.

Receiver resultDelivery behavior
Any 2xxSuccessful
Network/DNS/TLS error, timeout, 408, 425, 429, or 5xxRetry within the delivery budget
3xxPermanent failure; redirects are not followed
Other 4xxPermanent failure
410 GonePermanent failure and endpoint disabled

There are at most 10 attempts, with nominal offsets from the first attempt of 0, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 12 hours, 24 hours, 48 hours, and 72 hours. Intermediate offsets include jitter. The last attempt is not earlier than 72 hours; worker delays can make it later, so 72 hours is not a hard expiry deadline. An attempt is counted when it starts, so an attempt interrupted on our side can use up a slot without a recorded outcome: the per-attempt journal lists outcomes, and the budget is an upper bound on the requests your endpoint receives.

A positive Retry-After on 429 or 503 is honored within the retry policy's bounds. Deliveries can become exhausted or suppressed, so monitor the delivery journal on the Developers → Webhooks & deliveries → Deliveries page — it lists deliveries for every endpoint, filtered by status and event type, and opens the per-attempt record of one delivery — and reconcile missing outcomes.

Endpoint requirements#

Use public HTTPS with a valid certificate and TLS 1.2 or newer. Private, loopback, local, and metadata-service destinations are not permitted. Redirects are not supported. Do not put credentials in the URL or return secrets in the response body.

Registration does not prove the endpoint is reachable. Send a signed webhook.ping after registration, from the endpoint's detail dialog in Developers → Webhooks & deliveries. It uses the common envelope, subject_type: webhook_endpoint, and data: {"ping": true}; acknowledge it without executing a payment action. Ping is not one of the 21 subscribable event types. A test delivery is sent by the request that triggers it and receives no automatic background retries: a failed test is reported as Failed in the delivery journal, and you send another test once the endpoint answers.

Deduplication and ordering#

Persist verified events in an inbox with a unique event-ID constraint before acknowledging. Repeated events receive a successful acknowledgement without repeating business effects. Keep the deduplication record with the corresponding business record.

Do not discard an unseen event only because its sequence or timestamp is lower than the last one received. A delayed reversal can still matter. After processing an event, GET the resource when making a decision about its current status.

Invoice fulfillment must also be idempotent by invoice/order ID. Different event IDs can describe the same accepted payment. Do not sum net_amount from invoice.late_payment and invoice.paid. See business processes.

Schema versions#

Route by the pair type and schema_version. The reference shows all supported variants. invoice.created currently emits version 2; the contract also describes version 1. invoice.expired has versions 1 and 2 for different invoice nomination states. Do not assume every event is version 1.

Unknown event types or schema versions must not trigger an unvalidated financial action. Preserve them for diagnosis and return the response appropriate to your receiver's durable-processing policy.

Payout events#

payout.completed, payout.failed, and payout.reversed are published at schema_version 2. There is no per-endpoint finance version to configure and no earlier payout payload to support. Handle the version explicitly; do not interpret every future version as version 2.

For payout schema 2, amount is the requested payout amount, recipient_amount is the amount sent to the recipient, and target_funding_amount is the obligation in the payout asset. It is not a total across source assets. Reconcile balance_movements by each exact currency/network pair; original and reversed credits/debits remain separate, including a net-zero movement after reversal. funding.sources records each source reserve, exchanged principal, earned conversion fee, returned source, and delivered target. Ordinary payouts carry funding: null.

The event records these facts after the completion, failure, or reversal in the same database transaction. A failed or reversed payout can retain completed conversion costs and target proceeds; never assume every source debit was refunded. Retries deliver the saved event bytes, so later balance changes do not rewrite an earlier notification.

A payout error_message: "funding_failed" means funding closed before outbound execution without covering the target obligation. The native amounts still include completed exchanges and earned conversion fees. Such an event has no outbound transaction or actual network-cost fields.