Skip to main content
Migrate Already selling? Move your customers to Keylight without re-issuing a single key.
Keylight
Blog
onboarding licensing stripe

The First 60 Seconds After Purchase

9 min read Nicolas Demanez — Founder

Everyone optimizes the checkout page. Almost nobody designs what happens after it.

The 60 seconds after a payment clears is the highest-intent window your product will ever get. The customer has already decided, already paid, and is sitting there with the app half installed and their attention fully on you. In most desktop apps that window is dead air: a Stripe receipt, a redirect to a generic thank-you page, and an email that may or may not arrive before they get distracted.

This post starts at the instant money changes hands. Everything before that moment, trial length, reminder timing, the expiry screen, is covered in from trial to paid. Assume that worked. Now design the handoff.

Why “check your email for your license key” is the wrong success page

It is wrong because email is asynchronous and the customer is here right now.

That sentence is the whole argument. You have a human being on a page you control, with proven intent, and you are choosing to route them into a channel with variable latency, spam filtering, and a decent chance they typed their address wrong at checkout. Every one of those failure modes turns a completed purchase into a support ticket, and you built the ticket on purpose.

The delays are real. Transactional mail is usually fast, but “usually” is doing heavy lifting. Provider queuing, greylisting, and corporate mail gateways all add minutes. Gmail’s promotions tab swallows a good share of what does arrive. And a customer who does not see a key within about a minute starts forming a theory, and the theory is that your checkout broke.

So the success page shows the key. Rendered on the page, selectable, with a copy button. The email still goes out, because the customer will need that key again in eight months when they get a new laptop, but the email is the durable backup copy, not the delivery mechanism.

The second thing that page needs is a one-click path into the app that carries the key with it. A deep link like myapp://activate?key=... that the app handles and prefills. The customer should not be copying a string between a browser and a text field. Every manual transcription step is a chance to drop a character and land in your inbox complaining that the key does not work, which is the exact ticket class covered in reducing license activation support tickets.

The timeline, second by second

Here is what should happen and roughly when.

Second 0. The card clears. Stripe fires checkout.session.completed.

Seconds 0 to 3. Your webhook receives the event, verifies the signature, resolves what was bought, and provisions the license. This has to be fast, because it is racing the browser redirect. The mechanics of that pipeline, including idempotency on the session ID, are covered in the Stripe webhook pipeline, so I will not re-explain them here. What matters for this post is only the latency budget: provisioning should finish in single-digit seconds, and you need defined behavior for when it does not.

Seconds 2 to 5. Stripe redirects the browser to your success URL. Note the overlap with the previous step. That overlap is the race, and it is the part most implementations skip.

Seconds 5 to 15. The success page resolves the license and renders it: the key itself, a copy button, and a primary button that deep-links into the app with the key attached.

Seconds 10 to 30. The email lands. It contains the same key, the same deep link, and a link to the account portal. It is a receipt plus a recovery path.

Seconds 15 to 60. The customer clicks the deep link, the app opens, the key is prefilled, activation completes, and the app says so in plain language.

If your flow does not have a defined answer for each of those blocks, the gaps are where your refund requests come from.

Handling the race: the customer beats your webhook

Sometimes the browser redirect wins and the customer arrives before the license exists. Design for it explicitly.

There are three possible behaviors and two of them are bad. Rendering an empty key field looks broken. Showing “no license found” is a false error that will generate an immediate support email and possibly an immediate refund request. The correct behavior is to poll with an honest waiting state.

// Success page. The Checkout session ID comes from the redirect URL:
// success_url = "https://yourapp.com/welcome?session_id={CHECKOUT_SESSION_ID}"

type Result =
  | { status: 'ready'; key: string; deepLink: string }
  | { status: 'pending' }   // webhook has not landed yet
  | { status: 'failed'; reason: string }

async function resolveLicense(sessionId: string): Promise<Result> {
  const deadline = Date.now() + 45_000
  let delay = 700

  while (Date.now() < deadline) {
    const res = await fetch(`/api/license?session_id=${encodeURIComponent(sessionId)}`)

    if (res.status === 200) {
      const { key } = await res.json()
      return {
        status: 'ready',
        key,
        deepLink: `myapp://activate?key=${encodeURIComponent(key)}`,
      }
    }

    // 404 here means "not provisioned yet", NOT "does not exist".
    // Anything else is a real failure and should stop the loop.
    if (res.status !== 404) {
      return { status: 'failed', reason: `unexpected_${res.status}` }
    }

    await new Promise((r) => setTimeout(r, delay))
    delay = Math.min(delay * 1.5, 4_000)   // back off, do not hammer
  }

  // Timed out. The purchase is still valid. Say exactly that.
  return { status: 'failed', reason: 'timeout' }
}

Two details in there carry most of the weight. First, a 404 during the polling window means “not yet”, not “never”, and conflating those two is what produces the false error. Second, the timeout message must never imply the purchase failed. The money moved. Tell the customer their payment succeeded, that the key is being prepared, that it will arrive by email shortly, and give them a support link. A customer who reads “payment received, key on its way” waits. A customer who reads “error” asks their bank for the money back.

Keep the polling window generous, around 45 seconds, and back off as you go. If you regularly hit the timeout, that is not a frontend problem. That is your provisioning step being too slow, and it belongs on the dashboard next to the other numbers in the licensing metrics worth tracking.

The device change is the case that actually breaks

Someone buys on a phone and installs on a desktop. This is the single most common broken case, and deep links do not solve it.

A deep link only works on a device that has the app. Buy on an iPhone while your Mac is in another room and myapp://activate resolves to nothing. Any flow whose only handoff is a deep link has just stranded a paying customer.

Three things make this survivable, and you need all three:

  1. The email. It is the cross-device transport. This is the real reason to send it, not as primary delivery but as the copy that follows the customer to whatever machine they end up on.
  2. A portal the customer can sign into. Any device, any time, key retrievable on demand. This is also what handles the new-laptop case two years from now, and it is what a customer portal is for.
  3. A key format that survives being typed by hand. Grouped, uppercase, unambiguous characters, and an input that tolerates whitespace and case. Some fraction of customers will read the key off a phone screen and type it into a Mac. Design for the human eye. The format tradeoffs are in how license keys work.

Also handle the deep link on first launch, before the customer has ever opened the app. A brand new install that receives an activation URL should process it during startup rather than dropping it because some onboarding state machine has not finished. Test that specific path on a clean machine. It is the one everybody breaks, because developers always test on a device where the app has already run.

What the first in-app moment should say

Confirm the purchase state explicitly. Do not silently switch to full features.

The instinct is that the best activation is invisible: key accepted, every paid feature turns on, no interruption. That is wrong, and it is wrong for one specific reason. A customer who cannot tell whether it worked will email you to ask. Ambiguity is the thing that generates the ticket, not friction.

So say it. A short confirmation that names the plan and its scope: “Pro activated. Licensed to [email protected]. 3 of 3 devices available.” Three facts, one line each, no celebration animation. That message answers the three questions a new customer actually has, which are did it work, what did I get, and what are my limits. The device count is worth showing early because it is what people get confused about later, and the mechanics are in how device activations work.

Then point at the single most valuable thing the app does. Not a feature tour. One action.

Fast refunds are usually activation failures

Refunds in the first 24 hours are disproportionately delivery failures, not product rejection.

This is the framing that makes the whole flow worth funding. A refund at day 20 is a real product judgment and you should learn from it. A refund at hour two, from a customer who never activated, is almost never a verdict on your software. It is someone who paid, could not get in, and decided you were not worth the email.

You can check this directly. Join your refunds against activation records and split them by whether the customer ever successfully activated. If a meaningful share of your fast refunds come from accounts with zero activations, you do not have a product problem. You have a handoff problem, and it is fixable in an afternoon. What happens to the key after the refund is a separate subject, covered in refunds in licensed software.

That is what turns this from polish into a revenue mechanism. Recovering even a third of never-activated refunds is real money, and unlike most conversion work it does not require you to convince anyone of anything. They already bought.

So it is worth building properly, and you can build all of it yourself. Plenty of people should.

Concretely: a webhook endpoint with signature verification and idempotency, a license record with a lookup by Checkout session ID, an endpoint that distinguishes “not yet” from “not found”, a transactional email with correct DNS records, a deep link scheme registered and handled at cold start, and a portal with authentication so customers can retrieve keys later. None of it is hard. It is maybe a week, and then it is permanent maintenance that gets touched twice a year and breaks silently in between.

The alternative is a licensing layer that already has these pieces, because they are the same pieces every licensed app needs. Keylight provisions on the Stripe event, exposes the license by session ID so your success page can poll it, sends the email, and hosts the portal. Stripe stays your payment processor and keeps owning the money entirely. See pricing for the cost, or the Stripe integration page for what gets handled at each step.

Either way the design is the same, and it is worth writing down before you build anything. Show the key on the page. Email a copy. Deep link into the app with the key attached. Poll honestly during the race. Assume the buying device is not the installing device. Confirm the state out loud. Do that and the 60 seconds after payment stops being the weakest minute in your funnel.

If you have run this flow and found a failure mode worth adding, send us your feedback.

Frequently asked

Should the success page show the license key or just say check your email?+

Show the key. Email is asynchronous and can take minutes or land in spam, but the customer is on your success page right now with maximum intent. Send the email too, as a durable backup copy, not as the primary delivery channel.

What happens if the customer lands on the success page before the license is provisioned?+

Poll for it. The success page should carry the Checkout session ID, ask your backend for the license, and show an explicit waiting state until provisioning finishes. Never render a blank key field or a false error during the race.

How do I handle someone who buys on their phone but installs on a desktop?+

Assume it will happen and make the key retrievable later. A deep link is useless on a device without the app, so the flow needs an email copy and a customer portal the buyer can sign into from any machine.

Do fast refunds mean the product was wrong?+

Usually not. Refunds inside the first day are disproportionately activation failures rather than product rejection. Check whether the refunding customer ever activated before you conclude anything about product fit.

Ready to ship?

Create your account and start licensing your apps in under a minute. Free forever tier included.

Start Free