Sign in with AnyRouter
Let users sign in to your app with their AnyRouter account and run AI requests billed to them — no API keys to collect or store.
Add AI to your app without holding your users' API keys. A "Sign in with AnyRouter" button walks each user through a standard OAuth flow and hands your app a temporary, inference-only key — usage is billed to that user's own AnyRouter account (their credits or free tier), never to you.
The flow is OAuth 2.1 authorization code with PKCE. Your app is a public client: there is no client secret to protect, so the whole flow works from a browser-only app.
Before you start
- A public URL for your app (the page users return to after approving).
- No AnyRouter account is required to register your app — registration is open. Each of your users signs in with their own account.
Already have an AnyRouter key yourself and just want to mint an additional scoped key for a trusted local tool (a CLI, a localhost dev server)? That's a different flow — see OAuth: mint a key via PKCE. This guide is for the opposite case: your users have no AnyRouter key and need to sign in with their own account.
Quickstart
Register your app (once)
Register your app to get a client_id. Redirect URIs must be exact matches and use https (localhost is allowed for development).
curl https://anyrouter.dev/api/v1/mcp/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My AI App",
"redirect_uris": ["https://myapp.example/callback"],
"app_type": "signin",
"origin_url": "https://myapp.example"
}'
The response includes your client_id. Save it — it is public and safe to embed in frontend code.
Add the sign-in button
Generate a PKCE verifier/challenge in the browser, then send the user to the authorize page:
<button id="signin-anyrouter">Sign in with AnyRouter</button>
<script>
const CLIENT_ID = "your_client_id"
const REDIRECT_URI = "https://myapp.example/callback"
function base64url(bytes) {
return btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
}
document.getElementById("signin-anyrouter").addEventListener("click", async () => {
// PKCE: random verifier, S256 challenge
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)))
const challenge = base64url(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)),
)
const state = base64url(crypto.getRandomValues(new Uint8Array(16)))
sessionStorage.setItem("ar_verifier", verifier)
sessionStorage.setItem("ar_state", state)
const url = new URL("https://anyrouter.dev/api/v1/mcp/oauth/authorize")
url.search = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: "code",
code_challenge: challenge,
code_challenge_method: "S256",
state,
}).toString()
location.href = url.toString()
})
</script>
The user lands on the AnyRouter consent screen, sees your app's name and the exact permissions requested, and approves.
Exchange the code on your callback page
After approval, the user returns to your redirect_uri with ?code=…&state=…. Verify the state, then exchange the code:
<script>
const params = new URLSearchParams(location.search)
if (params.get("state") !== sessionStorage.getItem("ar_state")) {
throw new Error("State mismatch — restart sign-in")
}
const res = await fetch("https://anyrouter.dev/api/v1/mcp/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code: params.get("code"),
redirect_uri: "https://myapp.example/callback",
client_id: "your_client_id",
code_verifier: sessionStorage.getItem("ar_verifier"),
}),
})
const token = await res.json()
// {
// "access_token": "sk-ar-v1-…",
// "token_type": "Bearer",
// "scope": "inference read:profile",
// "expires_in": 2592000
// }
saveTokenForThisUser(token.access_token)
</script>
Run AI requests as the user
Use the token like any AnyRouter API key. The request is billed to the signed-in user's account.
const res = await fetch("https://anyrouter.dev/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-3.5-flash",
messages: [{ role: "user", content: "Hello from my app!" }],
}),
})
Show who's signed in
Fetch the user's basic profile to render a "Signed in as …" badge:
const me = await fetch("https://anyrouter.dev/api/v1/me", {
headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json())
// display name, avatar, plan
Permissions
A sign-in token carries exactly two scopes — nothing more:
| Scope | What it allows |
|---|---|
inference | /api/v1/chat/completions, /api/v1/messages, /api/v1/responses, /api/v1/embeddings — billed to the user |
read:profile | GET /api/v1/me — display name, avatar, plan |
What it can not do: create or revoke API keys, read usage or billing data, change account settings, or touch anything else in the user's account. There is no management access of any kind, and requesting extra scopes has no effect — the token is always limited to the two above.
Token lifetime and renewal
Sign-in tokens are temporary — 30 days by default. When a token expires, API calls return 401. There are no refresh tokens: just re-run the sign-in flow, and the user (if still signed in to AnyRouter) is bounced through consent and back with a fresh token in seconds.
Treat every 401 as "token expired, re-authenticate" — restart the authorize flow rather than showing an error.
Built-in MCP tools
The same token also works against AnyRouter's MCP server at https://mcp.anyrouter.dev/api/v1/mcp. Your app gets:
- Core tools — model catalog, credits, and other read-only helpers.
- Always-ready web tools — currently the Firecrawl tools (search, scrape, crawl), available when the user has Firecrawl connected in their AnyRouter dashboard.
Sign-in tokens are deliberately limited: management tools (key administration, admin operations) and the user's other MCP connections are not exposed to your app.
Revocation
Users stay in control: they can revoke your app's access at any time from Dashboard → Connected apps in their AnyRouter account. Revocation invalidates every token issued to your app for that user — subsequent requests return 401, and your app can simply offer sign-in again.
Best practices
- Store one token per user. Each token bills its owner's account. Never share a token across users or fall back to a shared key.
- Handle
401by re-authenticating. Expiry and revocation both surface as401— restart the sign-in flow instead of retrying. - Keep the
statecheck. It protects your users from login CSRF; the snippet above already does it. - Use exact redirect URIs. The URI in the authorize request and token exchange must match a registered one exactly, including path and scheme.
- Don't log tokens. Treat
sk-ar-v1-…values like passwords in your logs and error reporting.
Related
- Chat Completions API — the full request schema
- Errors — status codes and fixes
- MCP server — what the MCP endpoint offers
- App attribution — show up in app rankings