Overview
Create an app in the dashboard, then call these endpoints from your backend to register and sign in users, read their profile and plan, manage users, and open Stripe checkout. All requests and responses are JSON — send Content-Type: application/json on requests with a body.
Try it live
Fire real requests at this AuthRobo instance right from the browser. Start with GET /jwks (no auth) to check the connection, then paste a token or credentials to try the rest.
Public — the signing keys used to verify access tokens. No auth needed, so it doubles as a connection test.
// response appears here — hit Send
Requests run from your browser to this AuthRobo instance. Tokens and credentials you enter are sent only to the AuthRobo API and are never stored.
Authentication
The API uses three auth schemes, depending on the endpoint:
- App identifier — public end-user endpoints (
register,login,token) take yourclientIdin the JSON body. - User access token — pass the user's
access_tokenasAuthorization: Bearer <token>to read that user (/users/me) or start billing. - Client credentials — server-only admin endpoints use HTTP Basic with your
client_idas the username andclient_secretas the password. Never expose the secret in client code.
Get your credentials
Open the dashboard, create an app (paste your URL and we auto-fill the rest), and copy your AUTHROBO_ISSUER_URL, client_id and client_secret. The secret is shown once at creation.
New to what these values are and which one is secret? Your credentials & env vars (for dummies) →
AUTHROBO_ISSUER_URL and AUTHROBO_CLIENT_ID are public and safe to expose; AUTHROBO_CLIENT_SECRET is not. In Next.js, never give the secret a NEXT_PUBLIC_prefix — that ships it to every visitor's browser.Quickstart
Register a user and receive tokens:
curl -X POST https://authrobo.com/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"clientId": "arc_your_client_id",
"email": "dev@example.com",
"password": "SuperSecure123!",
"name": "Dev User"
}'
# → 201
# {
# "access_token": "eyJ...",
# "token_type": "Bearer",
# "expires_in": 900,
# "refresh_expires_in": 2592000,
# "refresh_token": "sess_....xxxx"
# }Then read the signed-in user with their access token:
curl https://authrobo.com/api/v1/users/me \ -H "Authorization: Bearer eyJ..."
Connect your AI (MCP)
AuthRobo is a Model Context Protocol server, so an AI agent (Claude, Cursor, VS Code, …) can work with your account by conversation. Every grant is scoped to your explicit consent, rate-limited, and logged — reads return non-sensitive data only, and your secrets never leave our servers.
Point your MCP client at this endpoint — that's the only value you need:
https://authrobo.com/api/mcp
The client discovers the rest automatically (OAuth 2.1 + PKCE, dynamic registration). On first connect, your browser opens AuthRobo's consent screen — sign in and approve. For clients that take a JSON config, it looks like this:
{
"mcpServers": {
"authrobo": {
"url": "https://authrobo.com/api/mcp"
}
}
}In Claude Desktop or Claude Code, add it as a remote/HTTP MCP server:
claude mcp add --transport http authrobo https://authrobo.com/api/mcp
What the agent can do
Read tools (apps:read, users:read) — each scoped to your apps, returning non-sensitive data only:
list_apps— your apps and their settingsget_app— one app's detailsget_enabled_methods— which sign-in methods are onget_domain_verification— custom-domain statuscount_users— how many end-users an app has (a number, no PII)
Write tools (apps:write) — never granted by default; a client must request them, and they show up distinctly on the consent screen. They create apps and change non-sensitive configuration on your apps, and each supports dry_run (on by default) to preview before applying:
create_app— create a new app; the client secret is never returned to the agent — you get a one-time link to reveal it in your browserset_method_enabled— turn a sign-in method on or offset_redirect_uris— replace an app's redirect URIsset_branding— edit name, description, and brand colourstart_domain_verification— get the DNS record and check status
Destructive tools (apps:delete, users:delete) — the agent can never delete anything itself. These only request a deletion: the tool returns an approval link, you open it in your browser, review exactly what would be removed, and confirm (or decline) out-of-band:
delete_app— request permanent deletion of an appremove_user— request removal of one end-user from an app
No tool ever reads or creates a secret, and no deletion happens without your explicit in-browser confirmation — so a hijacked or confused agent can, at worst, ask you to do something, never do it. Every call and every confirmation is written to a tamper-evident audit log.
API reference
Authentication
/auth/registerclientIdCreate an end-user under your app and return tokens. Password must be at least 8 characters.
{ "clientId": "arc_...", "email": "a@b.com", "password": "≥8 chars", "name": "Optional" }201 → { "access_token", "token_type", "expires_in", "refresh_expires_in", "refresh_token" }/auth/loginclientIdAuthenticate with email + password and return fresh tokens.
{ "clientId": "arc_...", "email": "a@b.com", "password": "..." }200 → { "access_token", "token_type", "expires_in", "refresh_expires_in", "refresh_token" }/auth/tokenrefresh_tokenExchange a refresh token for a new access token (and rotated refresh token).
{ "refresh_token": "sess_....xxxx" }200 → { "access_token", "token_type", "expires_in", "refresh_expires_in", "refresh_token" }/auth/exchangeclient_secret + PKCEConfidential client exchanges a one-time authorization code (+ PKCE verifier when used) for tokens. Codes are single-use.
{ "code": "...", "client_id": "arc_...", "client_secret": "ars_...", "code_verifier": "..." }200 → { "access_token", "token_type", "expires_in", "refresh_expires_in", "refresh_token" }Users
/users/meBearerVerify the access token and return the current user, including synced plan + subscription status.
200 → {
"id", "email", "name", "image", "app_id", "created_at",
"plan": "pro" | null,
"subscription_status": "active" | null
}/usersBasicList your app's users. Authenticate with Basic client_id:client_secret.
200 → { "users": [ { "id", "email", "name", "archived", "created_at" } ] }/users/:idBasicArchive or unarchive one of your users.
{ "archived": true } → 200 { "id", "archived": true }/users/:idBasicPermanently delete one of your users.
200 → { "id", "deleted": true }Billing
/billing/checkoutBearerCreate a Stripe Checkout session for the signed-in user. Requires the app to have Stripe connected.
{ "priceId": "price_...", "successUrl": "https://...", "cancelUrl": "https://..." }
→ 200 { "url": "https://checkout.stripe.com/..." }/billing/portalBearerOpen the Stripe billing portal so the user can manage their own subscription.
{ "returnUrl": "https://..." } → 200 { "url": "https://billing.stripe.com/..." }Keys
/jwkspublicJSON Web Key Set for verifying access tokens offline.
200 → { "keys": [ { "kty": "RSA", "kid": "...", "n": "...", "e": "AQAB" } ] }Verifying access tokens
Access tokens are RS256 JWTs. Verify them offline against the JWKS above — no network call per request. Check the signature plus:
iss= your issuer (https://authrobo.com)aud= yourclient_idexpnot passed. Thesubclaim is the user id.
import { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS = createRemoteJWKSet(new URL("https://authrobo.com/api/v1/jwks"));
const { payload } = await jwtVerify(accessToken, JWKS, {
issuer: "https://authrobo.com",
audience: process.env.AUTHROBO_CLIENT_ID,
});
// payload.sub = user idErrors
Errors use standard HTTP status codes with a JSON body:
{ "error": { "message": "human readable", "code": "machine_code" } }invalid_bodyRequest body failed validationno_token / bad_tokenMissing or invalid access tokenunauthorizedMissing/invalid client credentialscode_replayedAuthorization code already usedno_userUser not foundstripe_not_connectedApp hasn't connected StripeReact SDK
@authrobo/reactis a drop-in sign-in UI. It reads your app's enabled methods live from /api/v1/config and renders social buttons plus an email switcher — so the UI always matches your dashboard with no hard-coded lists.
npm i @authrobo/react
The whole widget
Wrap once, then drop in <AuthRoboAuth> (inline or mode="modal"):
import { AuthRoboProvider, AuthRoboAuth } from "@authrobo/react";
<AuthRoboProvider config={{
issuerUrl: "https://authrobo.com",
clientId: process.env.NEXT_PUBLIC_AUTHROBO_CLIENT_ID!, // arc_… (public)
redirectUri: "https://yourapp.com/auth/callback",
}}>
<AuthRoboAuth />
</AuthRoboProvider>The email switcher collects exactly each flow's fields — Magic Link (email), Register (name · email · password), Login (email · password). A tab shows only when its method (magic_link / password) is enabled.
Just one button
Prefer to build your own layout? <AuthRoboButton> renders a single branded button for any provider — use one, or map an array:
import { AuthRoboButton } from "@authrobo/react";
<AuthRoboButton provider="google" />
{["google", "linkedin", "github"].map((p) => (
<AuthRoboButton key={p} provider={p} />
))}Buttons are neutral by default (one themeable chrome). Pass buttonStyle="branded" to give each its provider colours — LinkedIn blue, GitHub black, Google white, Facebook blue, Apple black.
Prefetch & loading
<AuthRoboProvider> requests your enabled methods from /api/v1/config the moment it mounts and caches them — so a login modal opens instantly with no request-on-open. (No provider? Call prefetchAppConfig(config) on app start.) Any loading state uses a built-in theme-aware <Spinner>.
Direct vs. proxied (httpOnly cookies)
By default the widget talks to AuthRobo directly: you only need one callback route that exchanges the returned code. To keep the session in httpOnly cookies, pass startPath — every action is proxied through your own server, and password login/register set cookies with no page redirect (so onSuccess fires):
<AuthRoboAuth startPath="/api/auth" onSuccess={refreshSession} />With startPath="/api/auth", implement these routes (each forwards to AuthRobo and sets your cookies):
GET /api/auth/{provider}/start— state cookie + redirect to the provider startPOST /api/auth/login{ email, password } → password-grant, set cookiesPOST /api/auth/register{ name?, email, password } → create, set cookiesPOST /api/auth/magic{ email } → callmagic/startGET /api/auth/{method}/callback?code=…— exchange code (+ secret), set cookies
Return { "error": "…" }on failure and the widget shows it inline. Remember to register each callback URL under your app's Redirect URIs (any http://localhost:* callback is allowed automatically in local dev).
Theming
Self-contained and theme-aware. Override CSS variables on an ancestor to brand it — --authrobo-accent, --authrobo-input-bg, --authrobo-btn-bg, --authrobo-tabs-bg, and friends.
SDKs
The @authrobo/sdk TypeScript client wraps login, token refresh, offline verification, the PKCE authorization-code flow (createAuthorization / exchange), and billing helpers. The REST API works from any language — for token verification use any standard JWT/JWKS library (e.g. jose for Node, PyJWT for Python).
For the frontend, @authrobo/react gives you a themeable, drop-in sign-in UI (or a single provider button) — see the React SDK section.
FAQ
Which of my AuthRobo values are secret?+
Only AUTHROBO_CLIENT_SECRET. Your AUTHROBO_ISSUER_URL and AUTHROBO_CLIENT_IDare public by design (the client ID is your app's name tag). The secret is your password — keep it on the server only. Full explainer →
Do I need NEXT_PUBLIC_ env vars for AuthRobo?+
No. NEXT_PUBLIC_is a Next.js convention that inlines a variable into the browser bundle — it isn't something AuthRobo looks for. Read the plain AUTHROBO_* vars on the server and pass the public ones (issuer, client ID) to client components as props.
Why is my client secret showing up in the browser?+
You've exposed it — almost always a NEXT_PUBLIC_AUTHROBO_CLIENT_SECRET var or a secret read from client code. Next.js bakes anything NEXT_PUBLIC_-prefixed into the browser. Rotate the secret in the dashboard and read it only server-side.
What's the difference between my AuthRobo secret and my Google secret?+
Two different layers. Your AuthRobo client ID/secret identify your app to AuthRobo. A Google client ID/secret identify a provider app to Google, and you only need them if you bring your own Google app. They're never interchangeable.
Why does Google sign-in say “AuthRobo” instead of my app?+
Because you're using AuthRobo's shared Google app (the zero-setup default). To show your own brand on the consent screen, create your own Google OAuth client and paste it into the dashboard — step-by-step with screenshots →
