CheckoutAlarmWatch my app free

Supabase "Email link is invalid or has expired" (otp_expired): why the link fails and how to fix it

A user clicks the sign-in, confirmation or reset link in your Supabase email and lands on an error. Supabase's troubleshooting article lists what they may see: “Email link is invalid or has expired”, or a 403 on the verification endpoint. Supabase: OTP verification failures The error code is otp_expired. Supabase: Auth error codes Here are the causes and fixes Supabase documents.

What it would catch here

Nothing on this page. CheckoutAlarm doesn't see Supabase Auth emails, and it never sees your Auth errors.

What it won't catch

It doesn't sign up, log in, read inboxes or click links. It loads one page you pick and reads Stripe with a read-only key, so it can't tell you that sign-in links are failing.

Plans

Free: 1 site, checked once a day. Pro: $19 a month, up to 3 sites checked hourly. You prove you own the site first. For Stripe we only take a restricted key with read access to Events, Webhook Endpoints and Charges. Full secret keys are refused. Compare plans

The error code

Supabase describes otp_expired as: “OTP code for this sign-in has expired. Ask the user to sign in again.” Supabase: Auth error codes

Where you see it

When sign-in fails, Supabase still sends the user to your redirect URL, and “the error details will be returned as query fragments in the URL”: the part after #, with error_code and error_description. Read them from window.location.hash and show the user a clear message. Supabase: redirect URLs, error handling

Check the logs

Supabase suggests matching the 403 errors on the verification endpoint in your API logs with otp_expired entries in your Auth logs. It flags “rapid 403 errors or otp_expired messages shortly after an email is sent” as the sign of email prefetching (part 03, step 3). Supabase: OTP verification failures

The link was already used

“Magic Links only work with email addresses and are one-time use only.” Supabase: passwordless email sign-in A second click, or a click on a forwarded email after the first one, finds the link already spent. Supabase's advice for reset emails is to tell users not to forward them and to click the link directly. Supabase: OTP verification failures

The link is too old

By default, magic links and email OTPs expire after 1 hour. The Email OTP expiration setting also covers confirmation, password recovery, email change and invite links. Supabase: passwordless email sign-in A user who opens yesterday's email gets this error.

A security scanner opened it first

“Certain email providers may have spam detection or other security features that prefetch URL links from incoming emails.” Supabase names Safe Links in Microsoft Defender for Office 365 as an example. The {{ .ConfirmationURL }} is then consumed instantly, and the real user gets an expired or invalid token error. Supabase: email prefetching The troubleshooting article puts it this way: a scanner “can consume the OTP token by accessing the link before the legitimate user does.” Supabase: OTP verification failures

PKCE: opened in another browser

With the PKCE flow, the code verifier is “created and stored locally when the Auth flow is first initiated.” So “the code exchange must be initiated on the same browser and device where the flow was started.” The auth code is valid for 5 minutes and can only be exchanged once. Supabase: PKCE flow A user who asks for the link on a laptop and opens it on a phone breaks this rule.

PKCE: two sign-ins in one browser

If more than one PKCE flow is started in the same browser before either one completes, the later flow overwrites the stored code verifier, and exchanging the first flow's code fails. Supabase: PKCE flow Related codes: flow_state_expired (“Ask the user to sign in again”) and flow_state_not_found, which retried requests can cause. Supabase: Auth error codes

Less common: the clock

Supabase also lists a gap between the user's device clock and the server's time as a less common cause of tokens appearing invalid. Supabase: OTP verification failures

Ask for a new link

The quick fix for one user is the one in the error code: ask them to sign in again. Supabase: Auth error codes By default they can request a new magic link once every 60 seconds. Supabase: passwordless email sign-in Out of emails? See Supabase email rate limit exceeded.

Send a 6-digit code instead of a link

{{ .Token }} holds a 6-digit one-time password that can be used instead of the confirmation URL. Put it in the email template, link the user to a page where they enter their email and the code, then call supabase.auth.verifyOtp({ email, token, type: 'email' }). Supabase: email prefetching, option 1 The troubleshooting article gives the same advice: the link should only take the user to a page to enter the OTP. Supabase: OTP verification failures

Or link to a confirm button

Supabase's second option: link to a page of your own with a button, and put the real confirmation link in the button, for example {{ .SiteURL }}/confirm-signup?confirmation_url={{ .ConfirmationURL }}. Supabase: email prefetching, option 2 Your email provider may also offer its own ways to stop prefetching. Supabase: OTP verification failures

On PKCE, use the token hash template

For PKCE, Supabase edits the Magic Link template to send a token hash to your own endpoint: {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email. At /auth/confirm you call verifyOtp({ token_hash, type: 'email' }) to get the session. Supabase: passwordless email sign-in

Change the expiry if links get old

Go to Authentication, then Sign In / Providers, then Auth Providers, then Email, and set Email OTP expiration. Supabase says more than 86,400 seconds (one day) is strongly discouraged and can only be set with the Management API. Supabase: passwordless email sign-in

Turn off link tracking

If your external email provider enables “email tracking”, Supabase warns that the links in its email templates are overwritten and won't work as expected. Supabase: email tracking

Check where the link lands

If the link opens localhost:3000 or the wrong page, that's a Site URL or Redirect URLs setting: Supabase magic link redirects to localhost. If no email arrives at all: Supabase confirmation email not arriving.

What does otp_expired mean?
Supabase's error codes page says: "OTP code for this sign-in has expired. Ask the user to sign in again." Supabase's troubleshooting article lists "Email link is invalid or has expired" and 403 responses on the verification endpoint as what users see.
How long is a Supabase magic link valid?
By default a magic link or email OTP expires after 1 hour. You change it under Email OTP expiration in the Email provider settings. The same setting covers confirmation, password recovery, email change and invite links. Supabase strongly discourages more than 86,400 seconds (one day), and that can only be set with the Management API.
Can a magic link be used twice?
No. Supabase says magic links are one-time use only. Under PKCE, the auth code in the redirect is valid for 5 minutes and can only be exchanged once.
Why does the link fail before the user even clicks it?
Some email providers' security features open links in incoming emails. Supabase says the confirmation URL is then consumed instantly. Its fix is to send the 6-digit code with {{ .Token }}, or link to a page with a button that holds the real confirmation link.
Does CheckoutAlarm catch this error?
No. CheckoutAlarm loads one page you pick and reads Stripe with a read-only key. It doesn't sign up, log in or read email, so it never sees a Supabase Auth error.

CheckoutAlarm won't catch this. It loads one page you pick and reads Stripe with a read-only key. It doesn't sign up, read email or click links, so it never sees a Supabase sign-in link fail.

CheckoutAlarm is new. It only checks sites you prove you own. How to verify

Watch my app free

Docs checked on Oct 23, 2026.

Fixes for broken checkouts, by email

We'll email a link to confirm first. Nothing is sent until you click it.