Skip to content

Event: checkout.completed

Fired when a hosted-checkout session has provisioned a member, payment method, and active subscription.

A hosted-checkout session has finished successfully. Topiic has:

  1. Created or updated the Member from the session contact details.
  2. Stored a PaymentMethod with the gateway card token.
  3. Created an active Subscription on the merchant’s chosen Plan.

The event does not wait for the first scheduled charge to clear — if the plan has a trial, that charge is days away. Use this event to mark “the customer is signed up”, not “the customer has paid”.

HeaderValue
Content-Typeapplication/json
Topiic-Event-IdUUID — same across all delivery attempts for this event.
Topiic-Idempotency-KeyMirrors Topiic-Event-Id.
Topiic-Signaturet=<unix>,v1=<hex> — verify before processing. See Verifying signatures.
{
"id": "4f8c2a14-7b3d-4e9c-8d6f-1a2b3c4d5e6f",
"type": "checkout.completed",
"createdAt": "2026-05-27T10:25:12.123Z",
"data": {
"sessionId": "7e6d5c4b-3a2f-1e0d-9c8b-7a6f5e4d3c2b",
"externalRef": "cust_42",
"memberId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"paymentMethodId": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"subscriptionId": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"planId": "b6a8a5b8-7b3c-4d1e-9c2a-1f9e8d7c6b5a"
}
}
FieldTypeNotes
sessionIdUUIDThe Topiic checkout session id. Look it up in the portal under Webhooks → Deliveries for full audit detail.
externalRefstringWhatever you passed as ref in the original signed deep link. Use this to find the customer on your side.
memberIdUUIDThe Topiic Member id. Store it — most subsequent API calls take this as the customer key.
paymentMethodIdUUIDThe Topiic PaymentMethod id, holding the gateway token.
subscriptionIdUUIDThe active Topiic Subscription id.
planIdUUIDEchoes the plan from the deep link. Useful for fan-out logic if your product offers multiple tiers.
  1. Verify the signature. Reject anything that fails.
  2. Look up data.externalRef in your database. That’s the customer on your side.
  3. Persist data.memberId, data.subscriptionId, data.paymentMethodId on that customer row. You’ll need them for any subsequent API call.
  4. Mark the customer as “subscribed” — unlock whatever feature gates this entitlement.
  5. Return 2xx within 10 seconds.

The same event id can arrive more than once if your receiver was slow on the first attempt. Dedupe on id. See Retries & replay.

If you receive a checkout.completed for an externalRef that’s already linked to a different memberId, treat it as suspicious — probably a duplicate signup that hit Topiic before you blocked it on your side. Log it, do not blindly overwrite the existing link, and surface the discrepancy to support.

The webhook reports the plan the user was actually subscribed to (data.planId), which might differ from the plan id you sent if a merchant edited the plan in between. Treat data.planId as authoritative.

This can happen on a slow client. Your ?status=ok redirect handler should check your own database — if the customer is already marked subscribed (webhook beat the redirect), render success directly.

async function handleCheckoutCompleted(event, db) {
const { externalRef, memberId, subscriptionId, paymentMethodId, planId } = event.data;
await db.transaction(async (tx) => {
// Idempotency
const { rowCount } = await tx.query(
'INSERT INTO topiic_events (event_id) VALUES ($1) ON CONFLICT DO NOTHING',
[event.id]
);
if (rowCount === 0) return; // already processed
const customer = await tx.query(
'SELECT id FROM customers WHERE external_id = $1',
[externalRef]
);
if (!customer.rows[0]) throw new Error(`unknown externalRef ${externalRef}`);
await tx.query(
`UPDATE customers
SET topiic_member_id = $1,
topiic_subscription_id = $2,
topiic_payment_method_id = $3,
topiic_plan_id = $4,
status = 'active',
subscribed_at = NOW()
WHERE id = $5`,
[memberId, subscriptionId, paymentMethodId, planId, customer.rows[0].id]
);
});
}