Skip to content

Webhooks ​

Webhooks are HMAC-SHA256-signed event payloads delivered to your endpoint when something happens to a payment. They retry on failure and are the authoritative confirmation that a charge captured.

Events ​

Payment events ​

EventWhen
payment.completedSale captured successfully (terminal state for a Sale flow)
payment.capturedAuthorized payment moved to captured (separate Auth → Capture flow)
payment.capture_failedCapture attempt on an authorized payment failed
payment.failedPayment declined or failed
payment.refundedRefund processed (full or partial)
payment.refund_failedRefund attempt declined or errored — original capture stays settled
payment.voidedAuthorized payment voided before capture
payment.void_failedVoid attempt on an authorized payment failed

Subscription events ​

EventWhen
subscription.createdA new subscription was created (in trial or active)
subscription.activeA subscription moved into the active state (e.g. after trial end)
subscription.trialingTrial started — no charge yet
subscription.chargedSuccessful renewal cycle
subscription.charge_failedRenewal cycle declined — dunning starts on the merchant's schedule
subscription.cancelledSubscription cancelled (by merchant, customer, or after dunning)
subscription.updatedPayment method or schedule changed

Checkout-session events ​

EventWhen
checkout.session.expiredOpen session past its expires_at was swept (cron, every 5 min)
checkout.cancelledCustomer hit Cancel on the hosted checkout page

Payload ​

json
{
  "event_id": "evt_abc123",
  "event_type": "payment.completed",
  "payload_redacted": {
    "transaction_id": "txn_789xyz",
    "receipt_number": "RCT-000042",
    "amount": 2500,
    "currency": "USD",
    "status": "captured",
    "subtotal": 2300,
    "tax_amount": 200,
    "tax_name": "VAT",
    "payment_method_type": "card",
    "card_brand": "Visa",
    "card_last4": "0071",
    "token_id": "tok_abc123",
    "description": "Order #1042",
    "line_items": [
      { "name": "T-shirt", "quantity": 1, "unit_amount": 2300 }
    ],
    "customer": { "name": "Jane Doe", "email": "[email protected]" },
    "metadata": { "order_id": "1042", "source": "woocommerce" }
  }
}

payload_redacted is the safe-to-log payment data — no full PAN, no CVV, only last4.

Field availability by event type ​

Fieldpayment.*payment.refundedsubscription.*checkout.*
transaction_id✅✅✅—
receipt_number✅✅✅—
amount, currency✅✅ (refund amount)✅✅
subtotal, tax_amount, tax_name✅✅✅✅
payment_method_type✅✅✅—
card_brand, card_last4when cardwhen cardwhen card—
token_idwhen tokenizedwhen on-tokenalways—
line_items, description✅✅✅✅
customer✅✅✅✅
metadata✅ — caller-set metadata from the original session✅ — same metadata as the original session (NOT the refund's metadata.merchant_order_id)✅✅
checkout_session_external_id———✅
merchant_order_id (top-level on refunds)—✅——

metadata carries through unchanged from the POST /api/v1/checkout-sessions call you made — that's how storefront plugins (WC / Give / Magento / Shopify) correlate webhooks back to the original order. Set whatever you need (an order id, a SKU, a campaign tag) at session-create time and you'll get it back on every downstream event.

Subscription-cycle events ​

subscription.charged and subscription.cancelled carry the same shape as payment.* plus a subscription block:

json
"subscription": {
  "external_id": "sub_abc123",
  "plan_id": "plan_monthly_pro",
  "cycle": 7,
  "next_billing_date": "2026-06-12"
}

Checkout-session events ​

checkout.session.expired and checkout.cancelled skip the transaction_id field — there isn't one because no charge was attempted. Use them to clear orphan pending rows in your storefront:

json
{
  "event_id": "evt_xyz",
  "event_type": "checkout.session.expired",
  "payload_redacted": {
    "checkout_session_external_id": "cs_abc123",
    "amount": 2500,
    "currency": "USD",
    "metadata": { "order_id": "1042", "source": "give" }
  }
}

Signature Verification ​

Every webhook delivery includes four headers:

HeaderPurpose
X-GC-SignatureThe HMAC-SHA256 signature, hex-encoded
X-GC-TimestampUnix timestamp of the delivery attempt
X-GC-Event-IDUnique event id (evt_…) — also in the body, useful for log search
X-GC-Event-TypeThe event type (e.g. payment.completed)

Compute the expected signature server-side and constant-time compare:

expected = HMAC-SHA256("{timestamp}.{payload}", webhook_secret)

A complete PHP example is in Code Samples → Webhook Handler.

Reject stale events

Reject events with a X-GC-Timestamp more than 5 minutes off the wall clock. This blocks replay attacks even if a signature ever leaks.

Secret rotation ​

Each webhook endpoint has a current and a previous secret. To rotate:

  1. Dashboard → Webhooks → Endpoint → Rotate secret. A new secret is generated; the old one becomes the "previous" secret.
  2. New deliveries are signed with the current secret. Replays of pre-rotation events are signed with the previous secret (the platform tracks secret_version per delivery).
  3. Your receiver should accept either signature during a transition window — try the current secret first, fall back to the previous on mismatch.
  4. After all in-flight deliveries you care about have drained (24h is a safe ceiling), revoke the previous secret from the dashboard.

Retry Schedule ​

Failed deliveries retry with exponential backoff:

1m → 5m → 30m → 2h → 12h → 24h

Up to 6 attempts per event. After exhaustion the event is marked failed in the dashboard and you can replay it manually.

A delivery is considered failed if your endpoint returns any non-2xx response or times out (10 second limit).

Idempotency on the receiver ​

Make your handler idempotent on event_id (or transaction_id for payment events). The same event can deliver more than once — for example, when our retry logic and your server respond at the same time.

Next ​

Released under the proprietary Genius Checkout license.