Documentation

AuthRobo API

A REST API to add authentication and Stripe-synced billing to any app. Create an app in the dashboard, then call these endpoints from any backend — all over HTTPS with JSON.

Base URLhttps://authrobo.com/api/v1
GET/users/mePOST/auth/exchangeGET/jwks

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.

API console — live requests
GET

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 your clientId in the JSON body.
  • User access token — pass the user's access_token as Authorization: Bearer <token> to read that user (/users/me) or start billing.
  • Client credentials — server-only admin endpoints use HTTP Basic with your client_id as the username and client_secret as 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) →

Keep the secret server-side. 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 settings
  • get_app— one app's details
  • get_enabled_methods — which sign-in methods are on
  • get_domain_verification — custom-domain status
  • count_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 browser
  • set_method_enabled — turn a sign-in method on or off
  • set_redirect_uris— replace an app's redirect URIs
  • set_branding — edit name, description, and brand colour
  • start_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 app
  • remove_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

POST/auth/registerclientId

Create 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" }
POST/auth/loginclientId

Authenticate 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" }
POST/auth/tokenrefresh_token

Exchange 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" }
POST/auth/exchangeclient_secret + PKCE

Confidential 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

GET/users/meBearer

Verify 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
}
GET/usersBasic

List your app's users. Authenticate with Basic client_id:client_secret.

200 → { "users": [ { "id", "email", "name", "archived", "created_at" } ] }
PATCH/users/:idBasic

Archive or unarchive one of your users.

{ "archived": true }   →   200 { "id", "archived": true }
DELETE/users/:idBasic

Permanently delete one of your users.

200 → { "id", "deleted": true }

Billing

POST/billing/checkoutBearer

Create 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/..." }
POST/billing/portalBearer

Open the Stripe billing portal so the user can manage their own subscription.

{ "returnUrl": "https://..." }   →   200 { "url": "https://billing.stripe.com/..." }

Keys

GET/jwkspublic

JSON 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 = your client_id
  • exp not passed. The sub claim 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 id

Errors

Errors use standard HTTP status codes with a JSON body:

{ "error": { "message": "human readable", "code": "machine_code" } }
400invalid_bodyRequest body failed validation
401no_token / bad_tokenMissing or invalid access token
401unauthorizedMissing/invalid client credentials
401code_replayedAuthorization code already used
404no_userUser not found
409stripe_not_connectedApp hasn't connected Stripe

React 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 start
  • POST /api/auth/login { email, password } → password-grant, set cookies
  • POST /api/auth/register { name?, email, password } → create, set cookies
  • POST /api/auth/magic { email } → call magic/start
  • GET /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 →

Ready to build? Create your app →