APIS

Email verification belongs at the API boundary

Email verification should protect the API capability that needs it, while leaving sign-in, recovery, and account management usable.

On this page
  1. Authentication is not the same as capability access
  2. A blocked request should not look like successful usage
  3. Recovery is part of the security boundary
  4. The contract needs to be visible
  5. Put the gate where the value is

An unverified account should not automatically become a completely unusable account. If the thing you need to protect is API access, put the check in front of API access.

That sounds obvious, but authentication rules often get applied at the wrong layer. A product decides that an email address must be verified, then blocks sign-in, the dashboard, key management, and the verification flow itself. The user is left staring at a wall with no useful way through it.

I took the narrower approach in Bullion API. An unverified user can sign in, open the dashboard, manage their account, and receive the verification email. Their API key cannot read price data until the email is verified. The account is usable enough to recover, but the protected capability remains closed.

Authentication is not the same as capability access

There are two separate questions in this flow:

  1. Is this a real session or a valid API key?
  2. Is this account allowed to use the capability behind that credential?

The first question is authentication. The second is authorization. Email verification belongs in the second question for this product because the risk is API access, not the act of signing in.

That distinction is easy to lose when a framework exposes a single requireEmailVerification switch. A global switch is convenient, but convenience is not the same as a good product boundary. It can turn a narrow requirement into a broad lockout without making the system any safer where it matters.

The API already had an authentication boundary. Requests supplied an API key, the server checked whether it was valid, then quota accounting and request logging ran before price data was returned. The right place for the new rule was inside that request path, after key validation and before anything that represents successful API use.

This is the same kind of separation I want to see in any API authentication design: proving who or what is calling is one decision, deciding what that caller may do is another.

A blocked request should not look like successful usage

The most important detail in this change is not the 403 response. It is what does not happen before that response.

The API now validates the key and looks up the owning account's verification state in one shared access helper. If the key is missing, invalid, or revoked, the existing 401 responses remain unchanged. If the key is valid but its owner has not verified their email, the request receives a 403 with the stable email_not_verified code.

The helper returns before quota accounting and request logging. That means a blocked call does not consume the user's allowance and does not become a request record that looks like normal API activity. This matters for both fairness and diagnosis. A user who is waiting for an email should not lose quota while trying again, and an operator should be able to distinguish rejected access from work the API actually performed.

The order is the policy:

validate API key
check email verification
check quota
serve price data
log the request

Moving the verification check after quota accounting would still reject the request, but it would quietly charge the user for a capability they never received. Moving it after request logging would create misleading usage data. The response code tells the client what happened. The order protects the system from pretending something else happened.

This is also why consistent API error handling matters. A machine-readable error code gives the client a recovery path, while the server-side ordering keeps the event honest.

Recovery is part of the security boundary

The user experience is not a side concern here. It is part of whether the control works.

On sign-up, the application sends a verification email but still creates a session. The user can reach the dashboard, find the account controls, request another verification email, and manage their API keys. They cannot use those keys to read price data until verification is complete.

That gives the user a clear state: signed in, but waiting for one capability. It also gives the application a clear place to explain the problem. A dashboard can show the pending verification state, offer a resend action, and tell the user why API requests are returning email_not_verified. The product does not need to pretend the account is fully active, and it does not need to make recovery impossible.

I prefer this to blocking sign-in because a failed verification flow is not always the user's fault. The message may be delayed. The user may have mistyped their address. The email may have landed in spam. They may need to change the address or ask for another message. If sign-in is blocked, each of those cases becomes harder to resolve. If API access is blocked, the application can still help the user fix the state.

The boundary also avoids an awkward contradiction: requiring a user to verify their email before allowing them to access the screen that explains how to verify their email. Security controls should close the risky path, not remove the recovery path.

The contract needs to be visible

A new authorization state is an API contract change, even when the endpoint already returns errors. Clients need to know that a valid key can now receive 403, why it happens, and which code they should handle.

The OpenAPI description now documents the response on both price-data endpoints. The tests cover the unverified path, including the absence of quota usage and request logs. They also cover the successful verified path and preserve the existing missing, invalid, and revoked key responses.

That coverage is more useful than a single happy-path test. Authorization changes are good at breaking accounting and error semantics while leaving the returned data looking fine. The tests ask whether the request was rejected, whether it was rejected for the right reason, and whether the system avoided doing work it should not count.

Put the gate where the value is

Email verification is often described as an account-security feature, but the correct enforcement point depends on what the application is protecting. A dashboard may be safe to open while a data API is not. An account may need to remain available precisely so the user can complete the check.

The practical rule is simple: authenticate broadly enough for recovery, authorize narrowly enough to protect the capability. Keep the rejection response stable. Place the check before quota and side effects. Document the new state and test the work that must not happen.

That creates a product with a useful middle state instead of a binary one. The user is not fully trusted for API access yet, but they are not locked out of the process of becoming trusted either. For email verification, that is the boundary I want.

← Older
Filtering, Sorting, and Pagination
Newer →
Datetime Handling in REST APIs

Newsletter

A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.