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.