Authentication
Simbee authenticates server-to-server traffic with an API key. The official SDKs use the key directly — JWT exchange happens transparently under the hood. If you call the API by hand, this page covers the raw HTTP flow too.
Overview
You hold one credential: an API key. Everything else is plumbing.
- SDK path (recommended). Pass your API key to the SDK constructor and call methods. The SDK exchanges the key for a short-lived JWT on first use, caches it, and refreshes it before expiry. You never write a token-handling line of code.
- Raw HTTP path. One
POST /auth/exchangewith your API key returns a JWT. Use that token on subsequent calls until it expires (15 min default), then call/auth/exchangeagain.
Passwords aren't in the API path. The signup and email-login flows further down are for the admin dashboard — not for your application's API calls.
Using an SDK (recommended)
Pick your stack and pass the API key in. The SDK handles the exchange, caches the JWT for ~15 minutes, and refreshes ~30 seconds before expiry under a lock so concurrent requests don't stampede.
require "simbee-sdk"
client = Simbee::Client.new(api_key: ENV.fetch("SIMBEE_API_KEY"))
client.feed.ranked(user_id: "alice")That is the entire authentication story for the SDK path.
Raw HTTP — exchange API key for JWT
When you can't use an SDK (curl, an unsupported language, a one-off integration), trade your API key for a JWT once and reuse the token until it expires.
curl -X POST https://api.simbee.io/auth/exchange \
-H "Content-Type: application/json" \
-d '{"api_key": "'"$SIMBEE_API_KEY"'"}'Response:
{
"data": {
"token": "eyJhbGciOiJFUzI1NiIs...",
"expires_in": 900,
"scopes": ["admin", "read:user", "..."]
}
}The token is a 15-minute ES256 JWT signed by Simbee. When expires_in gets close to zero, call /auth/exchange again. There is no refresh token in this flow — your API key is the long-lived credential.
1. Sign up
Create a new tenant by providing an email, password, and company name. The response includes your client, an owner user, and a JWT you can use immediately.
curl -X POST https://api.simbee.io/auth/signup \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"password": "a-strong-password",
"company_name": "Acme Corp"
}'Response
// 201 Created
{
"client": {
"id": "cl_abc123",
"slug": "acme-corp",
"name": "Acme Corp",
"status": "active",
"tier": "graph"
},
"user": {
"id": "cu_xyz789",
"email": "alice@example.com",
"role": "owner"
},
"token": "eyJhbGciOiJFZERTQSIs...",
"scopes": ["admin", "read", "write"],
"expires_in": 900
}Save the client.id — you need it for all subsequent API calls. The initial token is valid for 15 minutes.
2. Create an API key
API keys are used for programmatic access. Create one using the initial token from signup. The raw key is returned only once — store it securely.
curl -X POST https://api.simbee.io/api/v1/clients/cl_abc123/api_keys \
-H "Authorization: Bearer eyJhbGciOiJFZERTQSIs..." \
-H "Content-Type: application/json" \
-d '{ "name": "production" }'Response
// 201 Created
{
"raw_key": "simbee_aBcDeFgHiJkLmNoPqRsTuVwXyZ",
"record": {
"id": "ak_def456",
"client_id": "cl_abc123",
"name": "production",
"fingerprint": "fp_a1b2c3",
"revoked": false,
"created_at": "2026-04-11T12:00:00Z"
}
}3. Exchange credentials for a JWT
Use /auth/token to exchange credentials for a short-lived JWT. Tokens are valid for 15 minutes (900 seconds) by default.
Email login
The simplest way to authenticate. Provide the email and password used during signup.
curl -X POST https://api.simbee.io/auth/token \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"password": "a-strong-password"
}'Programmatic login
For service-to-service or automated flows, authenticate with client_id and user_id instead of email.
curl -X POST https://api.simbee.io/auth/token \
-H "Content-Type: application/json" \
-d '{
"client_id": "cl_abc123",
"user_id": "owner",
"password": "a-strong-password"
}'Response
// 200 OK
{
"token": "eyJhbGciOiJFZERTQSIs...",
"scopes": ["admin", "read", "write"],
"expires_in": 900,
"user": {
"id": "cu_xyz789",
"email": "alice@example.com",
"role": "owner"
},
"client": {
"id": "cl_abc123",
"slug": "acme-corp",
"name": "Acme Corp"
}
}4. Make authenticated requests
Include the JWT in the Authorization header as a Bearer token on every API request.
# List users
curl https://api.simbee.io/api/v1/users \
-H "Authorization: Bearer eyJhbGciOiJFZERTQSIs..."
# Create a user
curl -X POST https://api.simbee.io/api/v1/users \
-H "Authorization: Bearer eyJhbGciOiJFZERTQSIs..." \
-H "Content-Type: application/json" \
-d '{ "external_id": "user_42", "traits": { "name": "Bob" } }'5. Token refresh
Tokens expire after expires_in seconds (default: 900s). When expired, the API returns 401 Unauthorized. Call POST /auth/token again with the same credentials to get a new token.
Recommended pattern:
- Store the token and its expiry time (
now + expires_in). - Before each request, check if the token is within 60s of expiry.
- If near expiry, call
/auth/tokento refresh. - On
401responses, re-authenticate and retry the request once.
class SimbeeAuth {
private token: string | null = null;
private expiresAt = 0;
constructor(
private email: string,
private password: string,
) {}
async getToken(): Promise<string> {
if (this.token && Date.now() < this.expiresAt - 60_000) {
return this.token;
}
const auth = new AuthenticationApi(
new Configuration({ basePath: "https://api.simbee.io" })
);
const { data } = await auth.createAuthToken({
email: this.email,
password: this.password,
});
this.token = data.token;
this.expiresAt = Date.now() + data.expires_in * 1000;
return this.token;
}
}6. JWKS verification (backend-to-backend)
To verify Simbee JWTs independently without calling the API, fetch public keys from the JWKS endpoint:
GET https://api.simbee.io/.well-known/jwks.jsonThe response contains the public keys used to sign session tokens. Use any standard JWT library to verify tokens against these keys.
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const client = jwksClient({
jwksUri: "https://api.simbee.io/.well-known/jwks.json",
cache: true,
rateLimit: true,
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
}
jwt.verify(token, getKey, { algorithms: ["EdDSA"] }, (err, decoded) => {
if (err) throw err;
console.log(decoded.sub); // User ID
console.log(decoded.aud); // Client ID
});kid, refresh the cache and retry once before rejecting.