Skip to content

Handling the return URL

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=ok
https://your-app.example.com/subscribed?customer=42&status=cancelled

status 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.com with a tab they close.
  • A user with two tabs can hit the return URL twice.
  • An attacker can fabricate a status=ok URL 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.

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 as cancelled.

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:

  1. On status=ok, immediately query your own database for the customer’s subscription state.
  2. If subscribed, show the success state.
  3. If not, render an interstitial that polls your own GET /api/me/subscription endpoint every 2s for up to 30s.
  4. 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=email

Topiic does not interpret, validate, or modify your other query parameters.

OutcomeFinal URLWebhook
User completes checkoutret?…&status=okcheckout.completed
User clicks Cancelret?…&status=cancelledcheckout.failed
Card capture erroredret?…&status=cancelledcheckout.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.