Quick start
Bototeka works with any OpenID Connect library that supports Authorization Code and PKCE S256.
Create an application
Xidmət adını, callback ünvanını və istəyə görə loqonu daxil edin. Google, Apple və Yandex callback-lərini Bototeka idarə edir.
Save credentials
Copy the Client ID and Client Secret. The secret is shown only once.
Configure OIDC
Give your library the issuer. It discovers every other endpoint automatically.
Test the flow
Start sign-in, validate state and nonce, exchange the code and create your local session.
- Issuer
- https://bototeka.com/api
- Discovery URL
- https://bototeka.com/api/.well-known/openid-configuration
- Service API
- https://api.bototeka.com/api/identity/v1
- Redirect URI example
- https://service.example/auth/callback
Sign-in and tokens
Your backend creates PKCE, state and nonce, sends the user to Bototeka and receives only a short-lived authorization code.
import { createHash, randomBytes } from "node:crypto";
const clientId = process.env.BOTOTEKA_CLIENT_ID;
if (!clientId) throw new Error("BOTOTEKA_CLIENT_ID is required");
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
const state = randomBytes(24).toString("base64url");
const nonce = randomBytes(24).toString("base64url");
const authorize = new URL("https://bototeka.com/api/oauth/authorize");
authorize.search = new URLSearchParams({
client_id: clientId,
redirect_uri: "https://service.example/auth/callback",
response_type: "code",
scope: "openid email profile",
code_challenge: challenge,
code_challenge_method: "S256",
state,
nonce,
}).toString();
// Save verifier, state and nonce in the user's server-side session.
console.log(authorize.toString());curl --request POST 'https://bototeka.com/api/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
--data-urlencode 'code=CODE_FROM_CALLBACK' \
--data-urlencode 'redirect_uri=https://service.example/auth/callback' \
--data-urlencode 'code_verifier=SAVED_PKCE_VERIFIER'curl 'https://bototeka.com/api/oauth/userinfo' \
--header 'Authorization: Bearer ACCESS_TOKEN'Registration data through the API
Create an API key in company settings and request customers or events for applications in your workspace.
/applications/{applicationId}/customersReturns sub, verified email, name and activity timestamps.
/applications/{applicationId}/eventsReturns registration or sign-in, amount, currency, status and event time.
curl 'https://api.bototeka.com/api/identity/v1/applications/APPLICATION_ID/customers?limit=50' \
--header 'Authorization: Bearer btk_v1_YOUR_API_KEY'curl 'https://api.bototeka.com/api/identity/v1/applications/APPLICATION_ID/events?limit=50' \
--header 'Authorization: Bearer btk_v1_YOUR_API_KEY'Reading requires identity:read. Send the key as a Bearer token and keep it on your server.
Webhooks
Subscribe in company settings. The endpoint must use HTTPS and the signing secret is shown only once.
identity.registration.completed.v1Registration completed and committed to the usage ledger.
identity.login.completed.v1A returning sign-in completed and was committed to the usage ledger.
Delivery is at least once. Deduplicate by event ID, accept events in any order and return 2xx only after saving the event.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyBototekaWebhook(rawBody, headers, secret) {
const timestamp = headers["x-bototeka-webhook-timestamp"];
const received = headers["x-bototeka-webhook-signature"];
if (!timestamp || !received) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = "v1=" + createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
const left = Buffer.from(expected);
const right = Buffer.from(received);
return left.length === right.length && timingSafeEqual(left, right);
}Production checklist
- Use an exact HTTPS Redirect URI and never accept a callback to an arbitrary destination.
- Generate a new state, nonce and PKCE verifier for every attempt and keep them in a server-side session.
- Never store Client Secrets, API keys, access tokens or refresh tokens in browser or mobile analytics.
- Request offline_access only when your backend truly needs a long-running session.
- Verify webhook signatures against the raw body and handle repeated delivery safely.
Errors and recovery
invalid_requestWhat happened
A required parameter is missing or invalid.
What to do
Check Redirect URI, PKCE, state, nonce and Content-Type.
invalid_grantWhat happened
The code expired, was used or does not match the verifier.
What to do
Start a new sign-in. Never retry the same code exchange.
temporarily_unavailableWhat happened
The application balance is too low.
What to do
Top up the OAuth balance and start a new sign-in.
429What happened
A request or verification-attempt limit was exceeded.
What to do
Honor Retry-After and retry with jittered backoff.
| Code | What happened | What to do |
|---|---|---|
| invalid_request | A required parameter is missing or invalid. | Check Redirect URI, PKCE, state, nonce and Content-Type. |
| invalid_grant | The code expired, was used or does not match the verifier. | Start a new sign-in. Never retry the same code exchange. |
| temporarily_unavailable | The application balance is too low. | Top up the OAuth balance and start a new sign-in. |
| 429 | A request or verification-attempt limit was exceeded. | Honor Retry-After and retry with jittered backoff. |
Knowledge base
When is usage charged?
After successful token issuance. A customer's first session is a registration and later sessions are sign-ins. Failed or cancelled attempts, refresh, UserInfo and logout are free.
Can I use email only?
Yes. Bototeka supports email registration, one-time codes, address verification and password recovery without social providers.
Can I connect a SPA?
Yes, as a public client with PKCE and a short-lived access token in memory. For production, a backend or BFF with an HttpOnly session is preferred.
What should I store as the customer ID?
Store the issuer and sub pair. Email may change and is not a stable identifier.
How do I rotate a secret?
Create or rotate the API key in company settings. Copy the new secret immediately, update server configuration and revoke old access.
Ready to connect
Create an application, save the secret and use the issuer from this guide.