Gift Card Webhook Events
Overview
The gateway emits eight gift-card lifecycle events. All events fire asynchronously
through the standard webhook delivery pipeline (WebhookDelivery table, signed
HMAC body, retries with exponential backoff). Subscribe via the Webhooks page in
the merchant dashboard or via WebhookSubscriptionsApi in either SDK.
All payloads are JSON. All currency amounts are integer cents in the gift card's
own currency (USD today; the gateway is currency-aware). All identifiers are
the gateway-assigned IDs (string for giftCardId, UUID string for transaction
IDs).
The event-type catalogue is asserted by WebhookServiceEventTypeContractTest
(see services/online-txn) — adding a new gift-card event must update both
WebhookEventTypes.kt and that contract test, otherwise CI fails.
Event types
gift_card.issued
Fires once when a new gift card is issued (single-issue or each row of a
bulk-issue stream). The card's plaintext number is never included — only
last4 for display.
{
"organizationId": "org_abc",
"giftCardId": "gc_01HX...",
"giftCardTransactionId": "8f2c...-uuid",
"initialBalanceCents": 5000,
"currency": "USD",
"expiresAt": "2027-04-19T00:00:00Z",
"issuedAtLocationId": "loc_01HX...",
"issuedToCustomerId": null,
"last4": "3452",
"actorType": "STAFF",
"actorId": "user_01HX..."
}
gift_card.redeemed
Fires when a card is debited at the point of sale or via online checkout.
amountCents is the positive redemption amount; the underlying ledger row
stores it as a negative delta but we publish the absolute value to keep
downstream accounting code symmetrical with gift_card.refunded.
{
"organizationId": "org_abc",
"giftCardId": "gc_01HX...",
"giftCardTransactionId": "8f2c...-uuid",
"amountCents": 1500,
"balanceAfterCents": 3500,
"locationId": "loc_01HX...",
"transactionId": "txn-uuid",
"actorType": "SYSTEM",
"actorId": "processing@gateway"
}
gift_card.balance_low
Fires once per crossing of lowBalanceThresholdCents (configurable in the
GiftCardConfig). Edge-triggered: the previous balance must be above the
threshold and the current balance at or below it. Reloads do not re-arm the
trigger until the balance climbs back above the threshold.
{
"organizationId": "org_abc",
"giftCardId": "gc_01HX...",
"giftCardTransactionId": "8f2c...-uuid",
"currentBalanceCents": 250,
"thresholdCents": 500,
"actorType": "SYSTEM",
"actorId": "processing@gateway"
}
gift_card.reloaded
Fires when a card balance is topped up via the reload endpoint, requires a
funding transaction in CAPTURED or SETTLED status with matching currency.
{
"organizationId": "org_abc",
"giftCardId": "gc_01HX...",
"giftCardTransactionId": "8f2c...-uuid",
"amountCents": 2500,
"previousBalanceCents": 500,
"balanceAfterCents": 3000,
"locationId": "loc_01HX...",
"fundingTransactionId": "txn-uuid",
"actorType": "STAFF",
"actorId": "user_01HX..."
}
gift_card.refunded
Fires when a refund of the original sale routes value back to a gift card
(direct path — see "Refund reroutes" below for the alternate path).
amountCents is the positive credit applied to the card.
{
"organizationId": "org_abc",
"giftCardId": "gc_01HX...",
"giftCardTransactionId": "8f2c...-uuid",
"amountCents": 1500,
"balanceAfterCents": 5000,
"transactionId": "refund-txn-uuid",
"actorType": "SYSTEM",
"actorId": "anonymous"
}
gift_card.expired
Fires from the gift-card-expiry-sweep Cloud Scheduler job for each card whose
expiresAt has passed and whose status was previously ACTIVE or REDEEMED.
The card's status moves to EXPIRED and the balance is zeroed in a single
atomic write.
{
"organizationId": "org_abc",
"giftCardId": "gc_01HX...",
"balanceAtExpiryCents": 1500,
"expiredAt": "2026-04-19T03:01:42Z"
}
gift_card.revoked
Fires when an Org admin or platform admin revokes a card (lost, fraud, customer
return). The card's status moves to REVOKED and the balance is zeroed; the
balanceAtRevocationCents field captures what was forfeited so accounting can
post a breakage entry.
{
"organizationId": "org_abc",
"giftCardId": "gc_01HX...",
"balanceAtRevocationCents": 1500,
"reason": "customer reported lost",
"actorType": "STAFF",
"actorId": "user_01HX..."
}
gift_card.adjusted
Fires when an Org admin or platform admin adjusts a card balance manually
(promo bump, dispute resolution, write-off). amountCents is signed: positive
for credits, negative for debits. The card's status follows: a card driven to
zero with reload disabled flips to REDEEMED; a positive adjustment from
REDEEMED flips back to ACTIVE.
{
"organizationId": "org_abc",
"giftCardId": "gc_01HX...",
"amountCents": 500,
"balanceAfterCents": 2500,
"reason": "promo bump",
"actorType": "STAFF",
"actorId": "user_01HX..."
}
Refund reroutes
A refund of the original sale normally routes the gift-card portion back to the
gift card (gift_card.refunded fires). When the gift card is REVOKED or
EXPIRED at refund time, the gift-card allocation is rerouted to the
primary tender instead. In that case:
gift_card.refundeddoes not fire for the rerouted allocation.- The refund transaction's metadata gains a
gift_card_refund_reroutesarray describing the rerouted allocations (giftCardId,originalGiftCardTransactionId,amountCents). - A
gift_card.refund_reroute_requirednotification email is sent to the configured ops alert recipients (gateway.gift-card.refund-reroute-alert-emails). - Two Micrometer counters are incremented:
gateway.gift_card.refund_reroutes.total{reason="revoked"|"expired"}gateway.gift_card.refund_rerouted_amount_cents.total{reason=...}
Dashboards: split the counters by reason to surface the operational
distinction between cards forfeited by merchant action (revoked) versus
cards aged out by the expiry sweep (expired).
Delivery semantics
| Property | Value |
|---|---|
| Delivery guarantee | At-least-once |
| Ordering | Not guaranteed across cards; per-card events arrive in source order on the writer side, but webhook delivery is concurrent |
| Retries | Standard webhook retry policy (exponential backoff, max 24h) |
| Signature | HMAC-SHA256 over the raw body, header X-Gateway-Signature |
| Idempotency key | giftCardTransactionId is unique per ledger row — use it to de-duplicate issued/redeemed/reloaded/refunded |
expired, revoked, and adjusted payloads do not surface a transaction ID
because the underlying ledger row's UUID is not part of the public payload
shape; if you need to de-dup these, use the tuple (giftCardId, eventType, balanceAfterCents, occurred_at) from your delivery log.
Related docs
- Webhook Event Log and Replay — persisted event API, replay, retention, and DLQ retry surface.