YouSeemLegit

Reference

API reference

A standards-compliant OAuth 2.0 and OpenID Connect provider. If your library speaks either, it already speaks this. New here? Start with the quickstart.

Overview

YouSeemLegit is an OAuth 2.0 authorization server with OpenID Connect on top, plus a hosted login page you brand. You register a client, send visitors to the authorize endpoint, and receive an authorization code at a redirect URI you control. Everything after that is three JSON endpoints over HTTPS.

Base URLhttps://youseemlegit.com
AuthorizeGET /api/oauth/authorize
TokenPOST /api/oauth/token
UserInfoGET or POST /api/oauth/userinfo
Client authHTTP Basic, or client_id + client_secret in the body
Body encodingform-urlencoded or JSON — both accepted
Access tokenJWT, 1 hour
Refresh token30 days, rotated on every use
PKCES256 or plain, optional for confidential clients

Key conventions differ by endpoint, because the OAuth endpoints follow the spec and the legacy ones keep compatibility. /api/oauth/* is snake_case only. /api/auth/token/refresh accepts and returns both. /api/auth/token/verify takes the key token.

The flow

  1. Redirect the visitor to /api/oauth/authorize with response_type=code, your client_id, redirect_uri and a state you generate.
  2. They authenticate on the branded login page — password, Google, LinkedIn, magic link, and 2FA if enabled.
  3. We redirect to your redirect_uri with code and your state, unchanged.
  4. Verify the state, then POST /api/oauth/token with grant_type=authorization_code.
  5. Use the access token. Read the profile from /api/oauth/userinfo, and rotate with grant_type=refresh_token before it expires.

Authorization codes are single-use. Redeeming one twice returns invalid_grant: code already used and is logged as a reuse event.

Authorize

GET/api/oauth/authorize— Start the flow
Request
GET /api/oauth/authorize
  ?response_type=code
  &client_id=cmo0n1i31...
  &redirect_uri=https%3A%2F%2Fyourapp.com%2Fapi%2Fcallback
  &scope=openid%20profile%20email
  &state=<your CSRF value>
  &code_challenge=<base64url(sha256(verifier))>
  &code_challenge_method=S256

# The user is sent to the branded /login page.
# When they finish, we redirect to your redirect_uri:
#
#   https://yourapp.com/api/callback?code=ac_9f2c...&state=<your value>
response_typeRequired. Must be "code".
client_idRequired. From your dashboard.
redirect_uriRequired. Must match a registered URI on scheme, host and path.
stateStrongly recommended. Returned to you verbatim.
scopeOptional. Echoed back in the token response.
code_challengeOptional. Enables PKCE.
code_challenge_methodOptional. "S256" (recommended) or "plain".

Error handling

When the client or redirect URI is invalid we do not redirect — that would make us an open redirector. The user lands on the branded login page with an error instead.

missing_parametersclient_id or redirect_uri was absent. The user is sent to the branded /login page with this error rather than redirected.
invalid_clientNo client matches that client_id. Shown on /login, not redirected.
invalid_redirect_uriThe redirect_uri is not registered for this client. Shown on /login — never redirected, since that would make us an open redirector.
unsupported_response_typeOnly response_type=code is supported. Redirected back to your redirect_uri with your state.

Token

POST/api/oauth/token— Both grants live here

Authenticate with HTTP Basic or by putting client_id and client_secret in the body. Public clients using PKCE may omit the secret.

grant_type=authorization_code

POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=authorization_code
&code=ac_9f2c...
&redirect_uri=https://yourapp.com/api/callback
&code_verifier=<the PKCE verifier, if you sent a challenge>

grant_type=refresh_token

Request
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=8f1c2e...
&client_id=cmo0n1i31...
&client_secret=cs_...

# Response: a new access_token AND a new refresh_token.
# The one you sent is consumed. Store the new one.

Refresh is a rotation: the token you send is replaced, and the response contains its successor. Persist the new one before you respond to your user, or a crash in between signs them out.

The id_token is signed with the same key as the access token. If you need to validate identity independently, call /api/oauth/userinfo rather than treating the two tokens as separately trusted.

UserInfo

GET/api/oauth/userinfo— Standard OIDC claims

This is where names and avatars come from. Pass the access token as a bearer token; both GET and POST are accepted.

GET /api/oauth/userinfo
Authorization: Bearer eyJhbGciOi...

# POST is also accepted, per the OIDC spec.
subStable user identifier. Store this, not the email.
emailAlways present, unique per user.
email_verifiedWhether the address has been confirmed.
nameDisplay name, when set.
given_name / family_nameWhen set, from signup or the provider profile.
pictureHTTPS avatar URL, when available.

Key your records on sub. A member can change their email address, and identity linking means one person may arrive through more than one provider.

Verify

POST/api/auth/token/verify— Cheap session check

Confirms an access token is valid and its session is live, and tells you how long is left. Useful as a per-request guard when you do not need profile fields.

POST /api/auth/token/verify
Content-Type: application/json

{ "token": "eyJhbGciOi..." }

// Note the key is "token" — not "access_token".

Two things that catch people out: the request key is token, not access_token; and an invalid token returns 200 with valid: false, not a 401. Branch on the valid field.

shouldRefresh turns true when under five minutes remain, so you can rotate opportunistically instead of tracking expiry yourself.

Legacy refresh

POST/api/auth/token/refresh— Dual key convention

An alternative to grant_type=refresh_token that accepts and returns both camelCase and snake_case. It exists for integrations that predate the OAuth endpoints. New code should prefer /api/oauth/token; this one is supported and not going away.

Request & response
POST /api/auth/token/refresh
Content-Type: application/json

{ "refreshToken": "8f1c2e..." }     // "refresh_token" also accepted

// Response carries BOTH key conventions:
{
  "token":         "eyJhbGciOi...",
  "access_token":  "eyJhbGciOi...",
  "refreshToken":  "3d90ab...",
  "refresh_token": "3d90ab...",
  "expiresIn":     3600,
  "expires_in":    3600,
  "expiresAt":     "2026-09-07T15:02:11.418Z",
  "tokenType":     "Bearer",
  "token_type":    "Bearer"
}

It does not require client credentials — possession of the refresh token is sufficient. Treat refresh tokens as secrets and keep them server-side.

Bulk import

POST/api/admin/users/import— Server-to-server, x-api-key

Move an existing membership over in batches of up to 1,000. Existing emails are skipped rather than duplicated, and a batch with some bad rows returns 207 with per-row reasons instead of failing wholesale. Image URLs must be HTTPS.

POST /api/admin/users/import
x-api-key: <your admin API key>
Content-Type: application/json

{
  "clientId": "cmo0n1i31...",
  "users": [
    {
      "email": "member@example.org",
      "givenName": "Jordan",
      "familyName": "Reyes",
      "displayName": "Jordan Reyes",
      "profileImageUrl": "https://cdn.example.org/a.jpg",
      "emailVerified": true
    }
  ]
}

Imported users are not asked to reset anything. Members who used a social provider keep using it; members with passwords keep theirs.

Errors

The OAuth endpoints return { error, error_description } with Cache-Control: no-store. Codes are stable; descriptions are for humans and may change — branch on the code.

400invalid_request

A required parameter is missing or malformed. error_description names it.

401invalid_client

Unknown client_id, or the client_secret did not match. Also returned when a confidential client omits both a secret and PKCE.

400invalid_grant

The code or refresh token is unknown, expired, already used, bound to another client, or the redirect_uri does not match the one used at authorize. Also returned when PKCE verification fails.

400unsupported_grant_type

grant_type must be authorization_code or refresh_token.

401invalid_token

From /api/oauth/userinfo: the bearer token failed verification or its session is gone. Carries a WWW-Authenticate header.

500server_error

Something failed on our side. Captured and alerted on — safe to retry with backoff.

/api/auth/token/verify is the exception: it answers 200 with valid: false instead of an error status.

Rate limits

Limits protect accounts from grinding, not your application from working. Verification is deliberately unlimited so you can check a token on every request.

Sign-in attempts5 per 15 minutes (sliding window)
Password reset requests3 per hour
OAuth initiation20 per 10 minutes
Token verificationNot rate limited — verify on every request
Bulk import1,000 users per request

Repeated failures against one account trigger a progressive lockout, which is reported separately from a rate limit. Back off with exponential delay and surface a wait message rather than looping.

Something missing here?

Tell us what you were looking for and we will write it.

Ask a question