Fired when a hosted-checkout session has provisioned a member, payment method, and active subscription.
When it fires
Section titled “When it fires”A hosted-checkout session has finished successfully. Topiic has:
- Created or updated the Member from the session contact details.
- Stored a PaymentMethod with the gateway card token.
- 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”.
Headers
Section titled “Headers”| Header | Value |
|---|---|
Content-Type | application/json |
Topiic-Event-Id | UUID — same across all delivery attempts for this event. |
Topiic-Idempotency-Key | Mirrors Topiic-Event-Id. |
Topiic-Signature | t=<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" }}data fields
Section titled “data fields”| Field | Type | Notes |
|---|---|---|
sessionId | UUID | The Topiic checkout session id. Look it up in the portal under Webhooks → Deliveries for full audit detail. |
externalRef | string | Whatever you passed as ref in the original signed deep link. Use this to find the customer on your side. |
memberId | UUID | The Topiic Member id. Store it — most subsequent API calls take this as the customer key. |
paymentMethodId | UUID | The Topiic PaymentMethod id, holding the gateway token. |
subscriptionId | UUID | The active Topiic Subscription id. |
planId | UUID | Echoes the plan from the deep link. Useful for fan-out logic if your product offers multiple tiers. |
What to do on receipt
Section titled “What to do on receipt”- Verify the signature. Reject anything that fails.
- Look up
data.externalRefin your database. That’s the customer on your side. - Persist
data.memberId,data.subscriptionId,data.paymentMethodIdon that customer row. You’ll need them for any subsequent API call. - Mark the customer as “subscribed” — unlock whatever feature gates this entitlement.
- Return 2xx within 10 seconds.
Edge cases
Section titled “Edge cases”Duplicate delivery
Section titled “Duplicate delivery”The same event id can arrive more than once if your receiver was slow on the first attempt. Dedupe on id. See Retries & replay.
Externalref already linked
Section titled “Externalref already linked”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.
Plan changed between link and webhook
Section titled “Plan changed between link and webhook”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.
Webhook arrives before return URL
Section titled “Webhook arrives before return URL”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.
Example handler (Node.js)
Section titled “Example handler (Node.js)”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] ); });}