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 on id.
  • 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 carry livemode: false.
  • data.object — the resource. There is no top-level object resource field and no data.resource; it is always data.object.
  • All resource field names are snake_case. Keys inside metadata are 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"
    }
  }
}
  • charge is the disputed charge's id; amount is in cents; reason is the processor's reason code.
  • status: needs_response (new chargeback), under_review (evidence submitted), won, lost.
  • dispute.won and dispute.lost reuse this shape with the final status. 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} using charge.
  • 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_id is the charge id; payment_instrument_id is a saved payment method you can charge again; customer_id is the customer the card was stored on (present whenever one was resolved, which is always in payment mode unless creation failed).
  • payment_method is card, apple_pay or google_pay.
  • mode: "subscription" adds subscription_id and subscription_status (created or failed, with subscription_error on failure).
  • metadata is the session's metadata with reserved _ keys removed; it is also copied onto the charge.
  • checkout.session.expired sends { 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 by POST /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.