Account API
Self-serve control surface. Dashboard/admin credential — never ship it in a product build.
Credential: as_ account session · scheme as_ account session
11 operations · 11 captured against the live API.
Account #
Create an account and its tenant
Signup is open self-serve, with no email verification. The email is trimmed and lowercased and must contain exactly one @. The password must be 12 to 256 bytes. Anything else is a 400. The call creates a new tenant (acct_…) owned by the account and logs you straight in, returning the same 30-day as_ session that login returns. Limited to 20 signups a minute per client IP and 400 a minute across all callers. Past that the answer is 429 rate_limited with Retry-After.
Request
curl -X POST https://api.ollacore.com/v1/account/signup \
-H "Content-Type: application/json" \
-d '{"email":"dev@example.com","password":"<redacted>"}' {
"email": "dev@example.com",
"password": "<redacted>"
} | Parameter | Type | Description |
|---|---|---|
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
|---|---|---|
email required | string (email) | Your email address; it becomes the login. Stored lowercased. Not verified: there is no email step. |
password required | string min length 12 · max length 256 | 12–256 characters. Stored only as an argon2 hash. |
Response · 200
{
"tenant_id": "acct_fac91b282191cc02dd29912c8db43f1c",
"session_token": "as_EXAMPLE_SESSION",
"expires_at": "2026-02-14T09:30:00Z"
} Response fields (3)
| Field | Type | Description |
|---|---|---|
tenant_id required | string | The account's auto-provisioned tenant. |
session_token required | string | Opaque bearer (as_...), shown once. |
expires_at required | string (date-time) | When this session stops working: 30 days after it was issued. Log in again for a new one. |
Errors · 400 · 409 · 429
| Status | When |
|---|---|
400 | An email without exactly one @, or a password outside 12–256 bytes. |
409 | email already registered |
429 | Over 20 signups a minute from this client address, or 400 a minute in all. |
Log in
Each successful login creates a new, independent session valid for 30 days. Sessions from earlier logins keep working until they expire or are logged out. A wrong password and an unknown email get the same 401. An email that is not shaped like an address gets 400 before any lookup. Limited to 30 attempts a minute per client IP and 600 a minute across all callers (429 rate_limited with Retry-After).
Request
curl -X POST https://api.ollacore.com/v1/account/login \
-H "Content-Type: application/json" \
-d '{"email":"dev@example.com","password":"<redacted>"}' {
"email": "dev@example.com",
"password": "<redacted>"
} | Parameter | Type | Description |
|---|---|---|
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
|---|---|---|
email required | string (email) | The email you signed up with. |
password required | string | Your password. |
Response · 200
{
"tenant_id": "acct_fac91b282191cc02dd29912c8db43f1c",
"session_token": "as_EXAMPLE_SESSION",
"expires_at": "2026-02-14T09:30:01Z"
} Response fields (3)
| Field | Type | Description |
|---|---|---|
tenant_id required | string | The account's auto-provisioned tenant. |
session_token required | string | Opaque bearer (as_...), shown once. |
expires_at required | string (date-time) | When this session stops working: 30 days after it was issued. Log in again for a new one. |
Errors · 400 · 401 · 429
| Status | When |
|---|---|
400 | The email is not shaped like an address. |
401 | Missing, expired or invalid credentials |
429 | Over 30 attempts a minute from this client address, or 600 a minute in all. |
Invalidate the session
Deletes only the session in the Authorization header. The account's other sessions and its API keys are untouched. Afterwards that token answers 401 everywhere, including on a repeated logout.
Request
curl -X POST https://api.ollacore.com/v1/account/logout \
-H "Authorization: Bearer as_…" \
-H "Content-Type: application/json" \
-d '{}' {} | Parameter | Type | Description |
|---|---|---|
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"logged_out": true
} Response fields (1)
| Field | Type | Description |
|---|---|---|
logged_out | boolean | True when this call removed the session. False only if a concurrent logout removed it first; either way the session no longer works. |
Errors · 401
| Status | When |
|---|---|
401 | Missing, expired or invalid credentials |
API keys #
List keys
Returns every key the account has minted, live and test, newest first. Revoked keys are included, with revoked_at set. Each entry carries id, name, environment, plan, scopes, created_at, last_used_at, revoked_at and expires_at. There is no pagination.
Request
curl -X GET https://api.ollacore.com/v1/account/api-keys \
-H "Authorization: Bearer as_…" | Parameter | Type | Description |
|---|---|---|
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"api_keys": [
{
"created_at": "2026-01-15T09:30:01Z",
"environment": "live",
"expires_at": null,
"id": "key-id-main",
"last_used_at": null,
"name": "docs-example-key",
"plan": "default",
"revoked_at": null,
"scopes": [
"rooms:read",
"rooms:write",
"members:write",
"tokens:mint",
"messages:write",
"webhooks:manage",
"usage:read",
"rooms:delete",
"data:erase"
]
}
]
} Response fields (1)
| Field | Type | Description |
|---|---|---|
api_keys | array of object | The account's keys, without secrets. |
Errors · 401
| Status | When |
|---|---|
401 | Missing, expired or invalid credentials |
Create a key (secret shown once)
environment must be live or test. A test key works in a separate sandbox namespace of your tenant and never sees live rooms, messages or usage. Omit scopes for the default set (rooms:read, rooms:write, members:write, tokens:mint, messages:write). You may also request webhooks:manage, usage:read, rooms:delete and data:erase. Any other scope name, or any other environment, is a 400. Keys made here are on the default plan and do not expire. Only a hash of the secret is stored, so api_key appears in this response and nowhere else.
Request
curl -X POST https://api.ollacore.com/v1/account/api-keys \
-H "Authorization: Bearer as_…" \
-H "Content-Type: application/json" \
-d '{"name":"docs-example-key","environment":"live","scopes":["rooms:read","rooms:write","members:write","tokens:mint","messages:write","webhooks:manage","usage:read","rooms:delete","data:erase"]}' {
"name": "docs-example-key",
"environment": "live",
"scopes": [
"rooms:read",
"rooms:write",
"members:write",
"tokens:mint",
"messages:write",
"webhooks:manage",
"usage:read",
"rooms:delete",
"data:erase"
]
} | Parameter | Type | Description |
|---|---|---|
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
|---|---|---|
name | string | Optional label for the key, for your own records. |
environment required | one of: live, test | live or test. A test key works in its own data namespace and never sees live data. |
scopes | array of one of: rooms:read, rooms:write, members:write, tokens:mint, messages:write, webhooks:manage, usage:read, rooms:delete, data:erase | Scopes to grant. Omit for the default set: rooms:read, rooms:write, members:write, tokens:mint, messages:write. usage:read, webhooks:manage, rooms:delete and data:erase must be asked for. |
Response · 200
{
"key_id": "key-id-main",
"api_key": "ak_live_EXAMPLE.NOT_A_REAL_KEY",
"environment": "live",
"scopes": [
"rooms:read",
"rooms:write",
"members:write",
"tokens:mint",
"messages:write",
"webhooks:manage",
"usage:read",
"rooms:delete",
"data:erase"
]
} Response fields (4)
| Field | Type | Description |
|---|---|---|
key_id required | string | The key's public id (ak_live_… or ak_test_…). Use it to rotate or revoke the key; it is not a credential. |
api_key required | string | Full ak_<env>_<id>.<secret>, shown once. |
environment required | one of: live, test | live or test. A test key sees only test data. |
scopes required | array of one of: rooms:read, rooms:write, members:write, tokens:mint, messages:write, webhooks:manage, usage:read, rooms:delete, data:erase | What the key may do. A call outside these scopes is refused with 403 when it is made. |
Errors · 400 · 401
| Status | When |
|---|---|
400 | An environment other than live or test, or an unknown scope. |
401 | Missing, expired or invalid credentials |
Rotate atomically
In one transaction, mints a replacement that carries the old key's name, environment, scopes, plan and expiry, and revokes the old key. The old secret is refused from its next request. Session tokens already minted with the old key stay valid until they expire. Not idempotent: if the response is lost, a retry gets 404 because the old key is already revoked. The replacement still exists and shows in GET /v1/account/api-keys, but its secret cannot be shown again, so revoke it and create a new key.
Request
curl -X POST https://api.ollacore.com/v1/account/api-keys/key-id-main/rotate \
-H "Authorization: Bearer as_…" \
-H "Content-Type: application/json" \
-d '{}' {} | Parameter | Type | Description |
|---|---|---|
key_id path required | string | The key's public id (ak_live_… or ak_test_…), as returned when it was created or listed. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"api_key": "ak_live_EXAMPLE.NOT_A_REAL_KEY",
"key_id": "ak_live_616b8de93a24dfbf2c5839186fd5b29a"
} Response fields (2)
| Field | Type | Description |
|---|---|---|
key_id | string | The replacement key's id. The old key is revoked in the same step. |
api_key | string | The replacement key, ak_<env>_<id>.<secret>. Shown once; store it now. |
Errors · 401 · 404
| Status | When |
|---|---|
401 | Missing, expired or invalid credentials |
404 | not an owned, live key |
Revoke
Sets the key's revoked_at. The key stays in the key list with that timestamp. The key's row is read on every request, so the next request made with this key gets 401. Session tokens it already minted stay valid until they expire. Revoking a key that is already revoked answers 404, which a retry can treat as done.
Request
curl -X DELETE https://api.ollacore.com/v1/account/api-keys/ak_live_e8177c7859fe3139043b58fff01924bd \
-H "Authorization: Bearer as_…" | Parameter | Type | Description |
|---|---|---|
key_id path required | string | The key's public id (ak_live_… or ak_test_…). |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"revoked": true
} Response fields (1)
| Field | Type | Description |
|---|---|---|
revoked | boolean | True once the key no longer works. |
Errors · 401 · 404
| Status | When |
|---|---|
401 | Missing, expired or invalid credentials |
404 | not an owned, live key |
Directory apps #
Provision an app (webhook or SMS provider)
Set exactly one of delivery_webhook_url or provider. In webhook mode the response carries delivery_secret, shown only here. Each OTP is POSTed to your URL as JSON {app_id, phone, code, expires_at} with an x-ollacore-signature: t=<unix seconds>,v1=<hex> header, where v1 is HMAC-SHA256 of <t>.<body> keyed with that secret. The URL is checked again on every delivery. In provider mode provider.kind is twilio, plivo or msg91. provider.config is checked for that vendor now, so a misconfiguration fails here rather than when a user asks for a code.
Request
curl -X POST https://api.ollacore.com/v1/account/directory-apps \
-H "Authorization: Bearer as_…" \
-H "Content-Type: application/json" \
-d '{"delivery_webhook_url":"https://hooks.example.com/hook"}' {
"delivery_webhook_url": "https://hooks.example.com/hook"
} | Parameter | Type | Description |
|---|---|---|
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
|---|---|---|
delivery_webhook_url | string (uri) | Webhook mode: where we POST each OTP, signed, for you to deliver. Must be a public https:// URL: one whose address is private, loopback or link-local is refused with 400 here, and every delivery checks again. A name that doesn't resolve yet is accepted now and refused at delivery if it still doesn't. Set this or provider, not both. |
provider | object | Provider mode: your own SMS vendor account, which we call to send the OTP. Set this or delivery_webhook_url, not both. Credentials cannot be updated later; rotate by creating a new app. |
provider. required if its parent is sent | one of: twilio, plivo, msg91 | Which SMS vendor. config holds that vendor's credentials and is validated for it now. |
provider. required if its parent is sent | object | Vendor-specific credentials; validated for kind at creation. |
Response · 200
{
"app_id": "da_03270a6c821fcca6c63a2049d93b62fe",
"delivery_mode": "webhook",
"delivery_secret": "dsec_EXAMPLE_NOT_A_REAL_SECRET",
"delivery_webhook_url": "https://hooks.example.com/hook"
} Response fields (4)
| Field | Type | Description |
|---|---|---|
app_id | string | The app's public id. Safe to ship in your mobile build; the directory endpoints take it to find your tenant. |
delivery_mode | one of: webhook, provider | How OTPs reach users: webhook or provider. |
delivery_secret | string, nullable | HMAC secret, shown once. Webhook mode only; absent in provider mode. |
delivery_webhook_url | string, nullable | The delivery webhook. Webhook mode only; absent in provider mode. |
Errors · 400 · 401
| Status | When |
|---|---|
400 | Invalid request: both or neither of delivery_webhook_url and provider; a provider configuration of the wrong shape; or a delivery_webhook_url that is not https:// or whose address is not public (private, loopback, link-local or otherwise reserved). A name that does not resolve yet is accepted. |
401 | Missing, expired or invalid credentials |
List apps and which push transports are configured
Newest first. Each entry has app_id, delivery_mode, delivery_webhook_url (webhook mode only) and created_at. It also has push_fcm and push_apns, which say whether native-push credentials are stored. The delivery secret, SMS-provider credentials and push credentials are never returned.
Request
curl -X GET https://api.ollacore.com/v1/account/directory-apps \
-H "Authorization: Bearer as_…" | Parameter | Type | Description |
|---|---|---|
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"directory_apps": [
{
"app_id": "da_03270a6c821fcca6c63a2049d93b62fe",
"created_at": "2026-01-15T09:30:01Z",
"delivery_mode": "webhook",
"delivery_webhook_url": "https://hooks.example.com/hook",
"push_apns": false,
"push_fcm": false
}
]
} Response fields (1)
| Field | Type | Description |
|---|---|---|
directory_apps | array of object | The account's directory apps. Secrets are never returned. |
Errors · 401
| Status | When |
|---|---|
401 | Missing, expired or invalid credentials |
Delete an app
The app_id stops working at once: otp/request and otp/verify answer 404 for it. Users belong to the tenant, not the app, so users, conversations and existing du_ sessions stay. Push tokens registered under the deleted app use the credentials of the tenant's only remaining directory app. They get no native push if the tenant has no other app or more than one.
Request
curl -X DELETE https://api.ollacore.com/v1/account/directory-apps/da_03270a6c821fcca6c63a2049d93b62fe \
-H "Authorization: Bearer as_…" | Parameter | Type | Description |
|---|---|---|
app_id path required | string | The directory app's public id. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Response · 200
{
"deleted": true
} Response fields (1)
| Field | Type | Description |
|---|---|---|
deleted | boolean | True once the app is gone. Its users and data stay in the tenant. |
Errors · 401 · 404
| Status | When |
|---|---|
401 | Missing, expired or invalid credentials |
404 | not an owned app |
Set FCM/APNs credentials (write-only)
Replaces both sides: an omitted or null fcm or apns clears what was stored, so send both to keep both. fcm.service_account needs project_id, client_email and private_key. A token_uri, if present, must be https://oauth2.googleapis.com/token. apns needs a PEM p8, key_id, team_id and topic (the bundle id), with env set to sandbox or production (default production). Credentials are validated here and are write-only: no call returns them. They sign pushes to the fcm and apns tokens registered under this app.
Request
curl -X PUT https://api.ollacore.com/v1/account/directory-apps/da_03270a6c821fcca6c63a2049d93b62fe/push \
-H "Authorization: Bearer as_…" \
-H "Content-Type: application/json" \
-d '{"fcm":{"service_account":{"project_id":"my-firebase-project","client_email":"push@my-firebase-project.iam.gserviceaccount.com","private_key":"-----BEGIN PRIVATE KEY-----\n<redacted>\n-----END PRIVATE KEY-----"}},"apns":{"p8":"-----BEGIN PRIVATE KEY-----\n<redacted>\n-----END PRIVATE KEY-----","key_id":"ABC123DEFG","team_id":"TEAM123456","topic":"com.example.chat","env":"production"}}' {
"fcm": {
"service_account": {
"project_id": "my-firebase-project",
"client_email": "push@my-firebase-project.iam.gserviceaccount.com",
"private_key": "-----BEGIN PRIVATE KEY-----\n<redacted>\n-----END PRIVATE KEY-----"
}
},
"apns": {
"p8": "-----BEGIN PRIVATE KEY-----\n<redacted>\n-----END PRIVATE KEY-----",
"key_id": "ABC123DEFG",
"team_id": "TEAM123456",
"topic": "com.example.chat",
"env": "production"
}
} | Parameter | Type | Description |
|---|---|---|
app_id path required | string | The directory app's public id. |
X-Request-Id header | string | Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID. |
Request fields
| Field | Type | Description |
|---|---|---|
fcm | object, nullable | FCM credentials: service_account is the Google service-account JSON. Omit or null to clear FCM, because this call replaces both sides. |
fcm. required if its parent is sent | object | Google service-account JSON (project_id, client_email, private_key, ...) |
apns | object, nullable | APNs credentials. Omit or null to clear APNs, because this call replaces both sides. |
apns. required if its parent is sent | string | The contents of the APNs .p8 key file (PEM). |
apns. required if its parent is sent | string | The APNs key's 10-character id. |
apns. required if its parent is sent | string | Your Apple developer team id. |
apns. required if its parent is sent | string | The app's bundle id. |
apns. | one of: sandbox, production default "production" | production (TestFlight and App Store) or sandbox (debug builds). Defaults to production. The wrong one fails silently. |
Response · 200
{
"push_apns": true,
"push_fcm": true
} Response fields (2)
| Field | Type | Description |
|---|---|---|
push_fcm | boolean | Whether FCM credentials are now stored. |
push_apns | boolean | Whether APNs credentials are now stored. |
Errors · 400 · 401 · 404
| Status | When |
|---|---|
400 | Invalid push config |
401 | Missing, expired or invalid credentials |
404 | No directory app with this id in your tenant. |
Errors on this plane #
Every failure uses the same body — code and message. These were
triggered deliberately against the live API.
401 · no Authorization header
{
"code": "unauthorized",
"message": "unauthorized: missing Authorization header"
} 401 · an as_ account session on the server plane
{
"code": "unauthorized",
"message": "unauthorized: malformed API key"
} The full code list and retry guidance is on Errors & rate limits.