Webhook Payloads
Every webhook is an HTTP POST with Content-Type: application/json, a User-Agent of HLHuntPay/1.0, and an X-Webhook-Signature header (t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">, keyed with the endpoint's whsec_ secret). Verification steps are on the Webhook Security page. Respond with any 2xx within 15 seconds; anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 24 hours (5 attempts in all), then marked failed.
Envelope
json
{
"id": "evt_3b1f0c9e4d2a4f8e9c7b6a5d4e3f2a1b",
"object": "event",
"type": "charge.succeeded",
"created": 1761580800,
"livemode": true,
"environment": "live",
"data": {
"object": { }
}
}id—evt_+ 32 hex characters. For outcome events that several code paths can announce (charge.succeeded,charge.failed,checkout.session.completed,dispute.*) the id is derived from the resource, so a redelivery and a duplicate announcement carry the same id: de-duplicate onid.type— the event name (see Webhook Events).created— Unix seconds.livemode/environment("live"or"test") — stamped when HL Hunt's test/live isolation is active. Treat a missing pair as live. Test events sent from the dashboard always carrylivemode: false.data.object— the resource. There is no top-levelobjectresource field and nodata.resource; it is alwaysdata.object.- All resource field names are snake_case. Keys inside
metadataare yours and are not rewritten.
charge.succeeded and charge.failed
data.object is the canonical charge object — byte-identical to GET /v1/charges/{id}. Metadata is echoed.
json
{
"id": "evt_9c2d5e7f1a3b4c6d8e0f2a4b6c8d0e1f",
"object": "event",
"type": "charge.failed",
"created": 1761580800,
"livemode": true,
"environment": "live",
"data": {
"object": {
"id": "TRxxxxxxxxxxxxxxxxxx",
"object": "charge",
"amount": 2000,
"fee": 0,
"currency": "USD",
"status": "failed",
"captured": false,
"description": "Order #1234",
"source": "PIxxxxxxxxxxxxxxxxxx",
"failure_code": "INSUFFICIENT_FUNDS",
"failure_message": "Your card has insufficient funds.",
"created": "2026-10-27T16:00:00.000Z",
"updated": "2026-10-27T16:00:01.000Z",
"metadata": { "order_id": "1234" },
"livemode": true,
"amount_refunded": 0,
"refunded": false
}
}
}For charge.succeeded the same object has status: "succeeded", captured: true and fee set to the processing fee in cents. failure_code and failure_message are null. After a full refund the charge reports status: "canceled", refunded: true.
These two events are emitted exactly once per charge outcome, for every payment path (API, Checkout Sessions, payment links, invoices, WooCommerce, Apple Pay / Google Pay, subscriptions). A disputed charge is never announced as failed. A declined API charge whose decline the processor recorded is announced as charge.failed.
charge.created (pending ACH debits only) carries the short create-response shape instead: { id, object: "charge", amount, currency, status: "pending", description, source, created, metadata }.
dispute.opened (and dispute.won / dispute.lost)
json
{
"id": "evt_1e2d3c4b5a69788796a5b4c3d2e1f0a9",
"object": "event",
"type": "dispute.opened",
"created": 1761580800,
"livemode": true,
"environment": "live",
"data": {
"object": {
"id": "c1n4x8k2q0003abcd9876efgh",
"object": "dispute",
"processor_dispute_id": "DIxxxxxxxxxxxxxxxxxx",
"charge": "TRxxxxxxxxxxxxxxxxxx",
"amount": 20940,
"currency": "USD",
"reason": "QUALITY",
"status": "needs_response",
"respond_by": "2026-11-07T00:00:00.000Z",
"created": "2026-10-01T18:22:10.000Z"
}
}
}chargeis the disputed charge's id;amountis in cents;reasonis the processor's reason code.status:needs_response(new chargeback),under_review(evidence submitted),won,lost.dispute.wonanddispute.lostreuse this shape with the finalstatus. When you submit evidence or accept a dispute from the dashboard, a shorter object is sent:{ id, object: "dispute", amount, reason, status: "evidence_submitted" | "accepted" }.- Charge metadata is not included on dispute events; fetch the charge with
GET /v1/charges/{id}usingcharge. - Bank returns of ACH payments are recorded as disputes but do not emit this event (see ACH & bank accounts).
checkout.session.completed
json
{
"id": "evt_7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"object": "event",
"type": "checkout.session.completed",
"created": 1761580800,
"livemode": true,
"environment": "live",
"data": {
"object": {
"id": "c1m2n3o4p0005qrstuvwx6789",
"object": "checkout.session",
"session_key": "cs_1a2b3c4d5e6f",
"status": "complete",
"payment_status": "paid",
"mode": "payment",
"amount_total": 5000,
"currency": "USD",
"customer_email": "jenny@example.com",
"customer_name": "Jenny Rosen",
"transaction_id": "TRxxxxxxxxxxxxxxxxxx",
"payment_instrument_id": "PIxxxxxxxxxxxxxxxxxx",
"payment_method": "card",
"customer_id": "c1m9k2x7e0001qz8f4h2b6n3p",
"metadata": { "order_id": "1234" }
}
}
}transaction_idis the charge id;payment_instrument_idis a saved payment method you can charge again;customer_idis the customer the card was stored on (present whenever one was resolved, which is always inpaymentmode unless creation failed).payment_methodiscard,apple_payorgoogle_pay.mode: "subscription"addssubscription_idandsubscription_status(createdorfailed, withsubscription_erroron failure).metadatais the session's metadata with reserved_keys removed; it is also copied onto the charge.checkout.session.expiredsends{ id, object, session_key, status: "EXPIRED", amount_total, currency, metadata }.
Other events
refund.created:{ id, object: "refund", amount, currency, status, charge, reason, created }.payment_method.created: the payment-method object returned byPOST /v1/payment-methods.customer.created/customer.updated: the customer object.payout.created: the payout object.- Subscription, invoice, billing-cycle, arrangement and usage-billing events carry the corresponding API objects; see their reference pages.
Test events from the dashboard
Developers → Webhooks → Send test event posts a synthetic event of the chosen type to one endpoint, signed with that endpoint's secret:
json
{
"id": "evt_test_…",
"object": "event",
"type": "charge.succeeded",
"created": 1761580800,
"livemode": false,
"data": {
"object": {
"id": "test_…",
"object": "charge",
"amount": 2000,
"currency": "USD",
"status": "succeeded",
"description": "Test event from HL Hunt webhook terminal",
"created": 1761580800
}
}
}Note the generic object: a test dispute.opened carries object: "dispute" with the fields above, not a real dispute shape. The Events tab's Resend sends a new synthetic event of the same type; it does not replay the original payload.