Your app calls Signet, and sign-in does not work. Find what you see in the table below, then go to its section. Each section says what you see, why it happens, and what to do next.
| What you see | Code | Go to |
|---|---|---|
| Every sign-up and sign-in fails with 403, even with the right password | INVALID_ORIGIN | Origin refused |
| Sign-in works, but the next page says you are signed out | none | Cookie missing |
| Signet will not send you back to your app | INVALID_CALLBACK_URL | Callback mismatch |
An email link lands with ?error= in the address | TOKEN_EXPIRED or INVALID_TOKEN | Expired link |
| No email arrives, or sign-in says the email is not confirmed | EMAIL_NOT_VERIFIED | Email not delivered |
| A feature is refused with 403 | starts with PLAN_FORBIDS_ | Plan refusal |
| Calls fail with 429 | none | Too many requests |
| Sign-up asks for a person check, or the check shows an error | MISSING_RESPONSE or VERIFICATION_FAILED | Person check |
Before you start
Look at what Signet sent back. Open your browser's developer tools, then the Network tab. Find the call to /api/auth/..., and read its status and its code. The Next.js starter shows the same facts on the form: open Details under the error.
Also check SIGNET_URL. It is your instance address plus /api/auth, for example https://your-instance.signetauth.dev/api/auth.
Origin refused
What you see. Sign-up and sign-in answer 403 with {"code":"INVALID_ORIGIN","message":"Invalid origin"}. The password is not the problem.
Why. Signet reads the Origin header of every call that changes something. It accepts the call only when that address is one of the instance's trusted origins. The instance's own address is always trusted. Your app's address, for example http://localhost:3000, must be added.
Signet sends the same two words as better-auth on purpose, so the stock client sees the answer it expects. The Next.js starter adds the missing facts. The form names the address that was refused, and the app's server log prints a line like this:
[signet] INVALID_ORIGIN on /api/auth/sign-in/email: Signet refused the origin "http://localhost:3000".What to do. Add your app's address to trusted_origins in the instance's signet.toml, then restart the instance:
[server]
trusted_origins = ["http://localhost:3000", "https://app.example.com"]Write the address exactly as the browser sends it: the scheme, the host and the port, with no path and no slash at the end.
On a hosted instance you cannot change this list yourself yet. Send your instance name and the address to support@signetauth.com, and we add it.
If the code is MISSING_OR_NULL_ORIGIN, the call had no Origin header. This happens when a server calls Signet. Send each browser call through your app's /api/auth route, which passes the browser's origin on.
Cookie missing
What you see. Sign-in answers 200, but the next page says you are signed out. The call to /api/auth/get-session answers null. The starter says that Signet accepted your password, but this browser did not keep the sign-in.
Why. Signet keeps you signed in with a cookie named better-auth.session_token. One of these stopped it:
- The browser calls Signet on another address. The cookie then belongs to the instance's address, so your app's server never sees it. Call
/api/authon your own app, and pass each call on to Signet from there. The starter's route handler does this. - The page is plain http on an address that is not localhost. A hosted instance marks the cookie
Secure. Browsers keep aSecurecookie only on https,localhostor127.0.0.1. Opening the app onhttp://192.168.1.20:3000loses it. - Your route handler drops or changes
Set-Cookie. Pass on everySet-Cookieheader, one by one, and remove itsDomainpart so the cookie belongs to your app. - The browser blocks cookies for your app. Allow cookies for its address.
How to check. In the developer tools, open Application, then Cookies, and pick your app's address. After sign-in, better-auth.session_token must be there.
Nothing is wrong with the account. Only the browser's sign-in was lost.
Callback mismatch
What you see. A call answers 403 with INVALID_CALLBACK_URL. The same check also answers INVALID_REDIRECT_URL, INVALID_ERROR_CALLBACK_URL or INVALID_NEW_USER_CALLBACK_URL, one for each return address.
Why. Signet sends people back only to a path, such as /dashboard, or to an address on a trusted origin. Anything else could send a signed-in person to someone else's site.
What to do. Use a path on your app as the return address. If you must use a full address, add its origin to trusted_origins first, as in Origin refused.
For sign-in with Google, GitHub or another provider, the provider checks one more address. It must list your instance's callback, https://your-instance.signetauth.dev/api/auth/callback/google, with the provider's name at the end. If it does not, the provider shows its own error page, for example Google's redirect_uri_mismatch.
Expired link
What you see. An email link brings you back to your app with ?error=TOKEN_EXPIRED or ?error=INVALID_TOKEN in the address. With no return address in the link, Signet answers 401 with the same code. The starter says the link has expired or was already used.
Why. Email confirmation links expire one hour after Signet sends them. INVALID_TOKEN means the link was cut short, or it does not match an account on this instance.
What to do. Sign in with your email and password. If Signet then says the email is not confirmed, ask for a new link. In the starter, press Send the link again. In your own app, call authClient.sendVerificationEmail({ email, callbackURL }). Nothing on the account changed.
Email not delivered
What you see. Sign-in answers 403 with EMAIL_NOT_VERIFIED. Or you wait for a confirmation or password reset email that does not come.
Why. The instance wants a confirmed email before anyone signs in. The account is saved, and it waits for the link to be used.
What to do.
- Wait a few minutes, then check the spam folder.
- Ask for a new link. Signet allows three email requests per minute from one network address.
- If no email ever arrives, look at the failed deliveries. On a hosted instance, open the console's Email section. On your own instance, run
signetctl deliveries. Each failed email is listed with the error, and you can send it again from there.
Plan refusal
What you see. A call answers 403 with a code that starts with PLAN_FORBIDS_, such as PLAN_FORBIDS_SSO or PLAN_FORBIDS_SERVICE_TOKENS. The rest of the code names the feature.
Why. The instance's plan does not include that feature. For example, single sign-on for companies starts on the Team plan.
What to do. Upgrade the plan in the console's Plan & account section, or leave the feature out of your app for now. Nothing was saved.
Too many requests
What you see. A call answers 429 with {"message":"Too many requests. Please try again later."} and no code. The X-Retry-After header says how many seconds to wait.
Why. Signet limits how often one address may call each route, to protect accounts. Sign-in and sign-up allow three tries in ten seconds. Routes that send email allow three per minute.
What to do. Wait for the number of seconds in X-Retry-After, then try again. The starter shows the wait on the form. Nothing was saved.
If every person behind your server shares one address, the limit hits all of them at once. Tell Signet which proxy to trust, as Deploying behind a reverse proxy shows. Rate limiting lists every rule and how to change it.
Person check
What you see. Sign-up answers 400 with MISSING_RESPONSE, or 403 with VERIFICATION_FAILED. The answer also has details.widget, with Cloudflare's script and a site key.
Why. The instance asks for Cloudflare Turnstile before it creates an account.
What to do. Show the widget, and send its answer in the x-captcha-response header. Each answer works once. The starter does this for you.
If the widget itself shows error 110200, the instance's site key does not allow your app's hostname. Fix this on the instance side. On a hosted instance, write to support@signetauth.com. Bot protection explains the settings.