BagsBags Docs
Api reference

Webhook Events

Complete reference for all webhook event types, payloads, and delivery behavior.

Bags sends webhook events to your registered endpoint when payment and subscription lifecycle events occur. All events are signed with HMAC-SHA256 for verification.


Event List

EventCategoryDescription
checkout.completedCheckoutHosted checkout finished successfully (payment settled on-chain)
checkout.failedCheckoutOn-chain transaction reverted or failed verification
checkout.cancelledCheckoutCustomer cancelled the checkout before broadcast
checkout.expiredCheckoutCheckout was not paid within its expiry window
payment.refundedPaymentA refund was processed against a completed transaction

Focus on checkout.* events. These four events cover the complete lifecycle of a hosted checkout created via POST /api/v1/checkouts. Use checkout.completed to fulfill orders — it's your source of truth for successful payments.

Subscription events coming soon. Recurring billing with subscription.* events is on the roadmap. Join the waitlist for early access.


Common Envelope

Every webhook delivery follows this structure:

{
  "event": "event.name",
  "data": { ... },
  "webhookDeliveryId": "d4e5f6a1-b2c3-7890-abcd-ef1234567890",
  "timestamp": "2026-04-01T12:00:00.000Z"
}
FieldTypeDescription
eventstringEvent type identifier
dataobjectEvent-specific payload (see below)
webhookDeliveryIdstringUnique delivery ID for idempotency
timestampstringISO 8601 timestamp of the event

Sandbox events also include "livemode": false in the data object.


Checkout Events

These events cover the full lifecycle of a hosted checkout created via POST /api/v1/checkouts. They are the primary source of truth for checkout state — use them to fulfil orders, release holds, and surface status in your UI.

checkout.completed

Fired when a checkout is fully paid and the on-chain transfer is verified.

{
  "event": "checkout.completed",
  "data": {
    "sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "paymentLinkId": "3b9f8f22-7f1f-4c3a-8f5a-2d7a0d3e9b1c",
    "txHash": "0xabc123def456...",
    "merchantWalletAddress": "0xYourWalletAddress",
    "amount": "29.99",
    "network": "base_sepolia",
    "livemode": false,
    "productId": "3b9f8f22-7f1f-4c3a-8f5a-2d7a0d3e9b1c",
    "externalCustomerId": "cus_123",
    "customerId": "4c0a9f33-8g2g-5d4b-9g6b-3e8b1e4f0c2d",
    "metadata": { "orderId": "ord_456" }
  },
  "webhookDeliveryId": "d4e5f6a1-b2c3-7890-abcd-ef1234567890",
  "timestamp": "2026-04-01T12:05:00.000Z"
}
FieldTypeDescription
sessionIdstringCheckout session ID (same as the id returned by POST /api/v1/checkouts)
paymentLinkIdstringEphemeral payment link that backed the checkout
productIdstringCatalog product id, when the link was product-backed
externalCustomerIdstringYour customer id, if supplied at session creation
customerIdstringBAGS customer row id, when reconciled
metadataobjectOpaque key/value metadata from session creation
txHashstringOn-chain transaction hash
merchantWalletAddressstringMerchant's receiving wallet
amountstringPayment amount in USD, sent as a decimal-precise string
networkstringbase, base_sepolia, ethereum, polygon, or solana
livemodebooleantrue for production, false for test mode

Recommended action: Fulfil the order, grant access, send a receipt. The checkout is now in a terminal complete state.

A matching payment.completed is emitted for backwards compatibility. Process one of the two events (not both) to avoid double-fulfilment.


checkout.failed

Fired when an on-chain transfer is broadcast but fails verification — either the transaction reverted, the wrong amount was transferred, or the destination address doesn't match the merchant's wallet.

{
  "event": "checkout.failed",
  "data": {
    "sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "paymentLinkId": "3b9f8f22-7f1f-4c3a-8f5a-2d7a0d3e9b1c",
    "txHash": "0xabc123def456...",
    "network": "base_sepolia",
    "amount": "29.99",
    "reason": "INVALID_TRANSFER",
    "message": "Transferred amount (10.00) does not match expected (29.99)",
    "livemode": false
  },
  "webhookDeliveryId": "d4e5f6a1-b2c3-7890-abcd-ef1234567890",
  "timestamp": "2026-04-01T12:05:00.000Z"
}
FieldTypeDescription
sessionIdstringCheckout session ID
paymentLinkIdstringPayment link that backed the checkout
txHashstring | nullThe reverted transaction, when one was broadcast
networkstringChain the transfer was attempted on
amountstringExpected amount in USD
reasonstringMachine-readable failure code (e.g. INVALID_TRANSFER, TX_REVERTED)
messagestringHuman-readable explanation
livemodebooleantrue for production, false for test mode

Recommended action: Notify the customer, surface message, allow retry. The checkout is in a terminal failed state and cannot be reused — issue a fresh POST /api/v1/checkout if the customer wants to try again.


checkout.cancelled

Fired when the customer explicitly cancels the checkout from the hosted page (or you call the cancel endpoint on their behalf) before any on-chain transaction is broadcast. Cancels after broadcast are rejected by the API because funds may already be moving.

{
  "event": "checkout.cancelled",
  "data": {
    "sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "paymentLinkId": "3b9f8f22-7f1f-4c3a-8f5a-2d7a0d3e9b1c",
    "network": "base_sepolia",
    "amount": "29.99",
    "reason": "customer_cancelled",
    "livemode": false
  },
  "webhookDeliveryId": "d4e5f6a1-b2c3-7890-abcd-ef1234567890",
  "timestamp": "2026-04-01T12:07:00.000Z"
}
FieldTypeDescription
sessionIdstringCheckout session ID
paymentLinkIdstringPayment link that backed the checkout
networkstringChain the customer had selected
amountstringExpected amount in USD
reasonstringAlways customer_cancelled in v0
livemodebooleantrue for production, false for test mode

Recommended action: Release any inventory holds, mark the order as cancelled, and optionally offer a "resume" CTA (which should create a new checkout — the cancelled one is terminal).


checkout.expired

Fired by the expiry cron when a checkout passes its expiresAt without a transaction being broadcast. Default expiry is 30 minutes from creation.

{
  "event": "checkout.expired",
  "data": {
    "sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "paymentLinkId": "3b9f8f22-7f1f-4c3a-8f5a-2d7a0d3e9b1c",
    "network": "base_sepolia",
    "amount": "29.99",
    "expiresAt": "2026-04-01T13:00:00.000Z",
    "livemode": false
  },
  "webhookDeliveryId": "d4e5f6a1-b2c3-7890-abcd-ef1234567890",
  "timestamp": "2026-04-01T13:00:12.000Z"
}
FieldTypeDescription
sessionIdstringCheckout session ID
paymentLinkIdstringPayment link that backed the checkout
networkstringChain the customer had selected
amountstringExpected amount in USD
expiresAtstringISO 8601 time the checkout was configured to expire
livemodebooleantrue for production, false for test mode

Recommended action: Release inventory holds, optionally email the customer with a fresh checkout link. The expired checkout is terminal; do not reuse its sessionId.

Expiry is evaluated every minute. The delivery timestamp may therefore be up to ~60 seconds after expiresAt. A checkout that was broadcast just before its expiry will still complete or fail through the normal payment events — the cron deliberately skips sessions with a broadcast transaction.


Refund Events

payment.refunded

Fired when a refund is processed against a previously completed transaction — either a full refund (reversing the entire payment) or a partial refund. Works for both standalone transactions and transactions that came through a checkout.* session.

{
  "event": "payment.refunded",
  "data": {
    "refundId": "rfd_2f9b...",
    "transactionId": "8c7e6d5f-1234-4abc-9def-0123456789ab",
    "txHash": "0xrefund789...",
    "merchantWalletAddress": "0xYourWalletAddress",
    "amount": "29.99",
    "subtotalAmount": "27.49",
    "taxAmount": "2.50",
    "feeReversed": "0.45",
    "token": "USDC",
    "network": "base_sepolia",
    "reason": "customer_requested",
    "fullyRefunded": true,
    "initiatedBy": "merchant",
    "livemode": false
  },
  "webhookDeliveryId": "d4e5f6a1-b2c3-7890-abcd-ef1234567890",
  "timestamp": "2026-04-02T09:15:00.000Z"
}
FieldTypeDescription
refundIdstringUnique refund ID
transactionIdstringOriginal transaction being refunded
txHashstringOn-chain hash of the refund transfer
merchantWalletAddressstringWallet the refund was sent from
amountstringTotal refunded amount (decimal-precise string)
subtotalAmountstringPortion of amount that is subtotal (pre-tax) reversed
taxAmountstringPortion of amount that is tax reversed
feeReversedstringPlatform fee reversed back to the merchant
tokenstringStablecoin refunded (e.g. USDC)
networkstringChain the refund was broadcast on
reasonstring | nullFree-text reason supplied at refund time
fullyRefundedbooleantrue if this refund zeroed the remaining refundable balance
initiatedBystringmerchant, api, admin, or customer
livemodebooleantrue for production, false for test mode

Recommended action: Reverse any fulfilment (revoke access, issue credit, mark order refunded). If fullyRefunded is false, the transaction can still receive further partial refunds up to the remaining balance.


Delivery and Retries

Bags retries failed deliveries up to 8 times with exponential backoff over ~24 hours:

AttemptDelay after failure
1Immediate
2~1 minute
3~5 minutes
4~30 minutes
5~2 hours
6~5 hours
7~10 hours
8~24 hours

After 8 failed attempts, the delivery is marked as failed. You can retry manually from the dashboard under Developer Settings > Webhooks.


What's next

On this page