Skip to content

Event: checkout.failed

Fired when a hosted-checkout session ended without a member + payment method + subscription being created.

A hosted-checkout session ended in a non-success terminal state. Reasons include:

  • The user clicked Cancel on the hosted page.
  • The gateway rejected the card during the hosted-form step.
  • Provisioning the member / subscription on Topiic’s side errored after card capture.

The event does not fire if the user simply closes the tab — the session sits in AwaitingPayment until it hits exp, at which point it transitions to Expired silently. Use a reconciliation pass to find these (see Handling the return URL).

Same as checkout.completed: Content-Type, Topiic-Event-Id, Topiic-Idempotency-Key, Topiic-Signature.

{
"id": "9d8c7b6a-5e4f-3a2d-1c0b-9a8f7e6d5c4b",
"type": "checkout.failed",
"createdAt": "2026-05-27T10:31:48.512Z",
"data": {
"sessionId": "7e6d5c4b-3a2f-1e0d-9c8b-7a6f5e4d3c2b",
"externalRef": "cust_42",
"failureReason": "User abandoned checkout."
}
}
FieldTypeNotes
sessionIdUUIDThe Topiic checkout session id. Look it up in the portal for full audit detail.
externalRefstringWhatever you passed as ref in the original signed deep link.
failureReasonstringHuman-readable reason. Not a stable identifier — do not switch on its value.
  1. Verify the signature. Reject anything that fails.
  2. Look up data.externalRef to find the customer on your side.
  3. Decide what state to put them in. Typically:
    • Mark the customer as “subscribe attempt failed” so they show up in a retry queue.
    • Do not delete the customer record — they may try again.
  4. Optionally surface the reason to your support team, but don’t show the raw string to the end user (it’s intended for staff).
  5. Return 2xx within 10 seconds.
failureReason substringLikely cause
User abandoned checkoutCustomer clicked Cancel or aborted via UI.
Checkout session expiredCustomer didn’t finish before exp.
Card capture failedThe gateway rejected the card token (declined, expired, fraud, etc).
Could not provision membershipInternal error after capture — Topiic’s logs will have detail.

Don’t pattern-match on this string in production logic — Topiic may refine the wording without notice. If you need to branch behaviour by failure type, ask for that to be elevated into a stable field.

async function handleCheckoutFailed(event, db) {
const { externalRef, sessionId, failureReason } = event.data;
await db.transaction(async (tx) => {
const { rowCount } = await tx.query(
'INSERT INTO topiic_events (event_id) VALUES ($1) ON CONFLICT DO NOTHING',
[event.id]
);
if (rowCount === 0) return;
await tx.query(
`UPDATE customers
SET status = 'subscribe_failed',
last_failure_reason = $1,
last_failed_at = NOW()
WHERE external_id = $2`,
[failureReason, externalRef]
);
await tx.query(
`INSERT INTO failed_signups (external_ref, session_id, reason, occurred_at)
VALUES ($1, $2, $3, NOW())`,
[externalRef, sessionId, failureReason]
);
});
}