What to do (and not do) when the user is redirected back from hosted checkout.
After the user finishes — successfully or otherwise — Topiic redirects the browser to the ret URL you embedded in the deep link, appending a status query parameter.
https://your-app.example.com/subscribed?customer=42&status=okhttps://your-app.example.com/subscribed?customer=42&status=cancelledstatus is one of ok (provisioning completed) or cancelled (user abandoned or capture failed).
The return URL is a UX cue, not a source of truth
Section titled “The return URL is a UX cue, not a source of truth”The user reaches the return URL through their browser. That means:
- A flaky network can drop the redirect — the customer ends up on
pay.topiic.comwith a tab they close. - A user with two tabs can hit the return URL twice.
- An attacker can fabricate a
status=okURL and visit it directly.
The webhook is the source of truth. It carries a signed payload Topiic mints server-to-server. Your “is this customer paid?” decision should always come from webhook state, not from the redirect.
What to render
Section titled “What to render”A pragmatic pattern:
status=ok— show “Thanks, you’re set up”. If your database already shows the customer as subscribed (the webhook beat the redirect), great. If not, render a “we’re activating your account, this usually takes a few seconds” interstitial and poll your own backend.status=cancelled— show “no worries, try again” with a fresh “Subscribe” button.- Anything else / no
status— treat ascancelled.
Reconciling delayed webhooks
Section titled “Reconciling delayed webhooks”The webhook usually lands within a couple of seconds of status=ok, but it can be slower if the receiver is busy or under load. Recommended UX:
- On
status=ok, immediately query your own database for the customer’s subscription state. - If subscribed, show the success state.
- If not, render an interstitial that polls your own
GET /api/me/subscriptionendpoint every 2s for up to 30s. - If still not subscribed after 30s, show “We’re still processing — we’ll email you when you’re set up” and trust the webhook (and its retries) to eventually land.
Pre-filling the return URL with your own state
Section titled “Pre-filling the return URL with your own state”You control the ret URL entirely — Topiic only appends &status=…. So embed whatever you need:
https://your-app.example.com/subscribed?customer=42&cohort=spring-promo&utm_source=emailTopiic does not interpret, validate, or modify your other query parameters.
Where the user actually lands
Section titled “Where the user actually lands”| Outcome | Final URL | Webhook |
|---|---|---|
| User completes checkout | ret?…&status=ok | checkout.completed |
| User clicks Cancel | ret?…&status=cancelled | checkout.failed |
| Card capture errored | ret?…&status=cancelled | checkout.failed |
| User closes tab mid-flow | (none — they don’t return) | (none until session expires) |
| Session expired | (depends on when expiry triggers) | (none — silent) |
For the “silent” cases, your reconciliation job should look for refs you generated more than 30 minutes ago that never received a webhook and either mark them abandoned or prompt the user to retry.