Skip to main content

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.refunded does not fire for the rerouted allocation.
  • The refund transaction's metadata gains a gift_card_refund_reroutes array describing the rerouted allocations (giftCardId, originalGiftCardTransactionId, amountCents).
  • A gift_card.refund_reroute_required notification 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

PropertyValue
Delivery guaranteeAt-least-once
OrderingNot guaranteed across cards; per-card events arrive in source order on the writer side, but webhook delivery is concurrent
RetriesStandard webhook retry policy (exponential backoff, max 24h)
SignatureHMAC-SHA256 over the raw body, header X-Gateway-Signature
Idempotency keygiftCardTransactionId 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.