Authentication & errors
API key types, scopes, and workspace rules; the two-layer rate limits and X-RateLimit-* headers; the uniform response envelope and 6-digit error codes.
The Public API authenticates every request with an API key passed as a bearer token:
Authorization: Bearer <key>A missing or invalid key is rejected before any handler runs — see the Api.*
codes under Errors below.
Key types
| Type | Prefix | Bound to | Acts as |
|---|---|---|---|
| Workspace | ti_ws_ | One workspace | A role stamped on the key at creation (default ADMIN, capped by the creator's role). |
| Personal | ti_pk_ | Your user | You, with your live membership role in the target workspace. |
Workspace rules
Every endpoint path includes a :workspaceId. The key must be authorized for that
workspace:
- Workspace key (
ti_ws_) — the path:workspaceIdmust match the workspace the key was created for. Using it against any other workspace returnsApi.WorkspaceForbidden(403). - Personal key (
ti_pk_) — you must be a member of the target workspace. Your effective role is your live membership role in that workspace; if your membership changes or is removed, access changes immediately. A non-member workspace returnsApi.WorkspaceForbidden(403).
Soft-deleted workspaces are always rejected.
Scopes
Each key carries a set of scopes. A request must hold the scope its endpoint
requires; otherwise it returns Api.ScopeForbidden (403). A scope is also capped
by the key's role in the workspace — a write scope held by a VIEWER is still
denied.
Most read scopes are capped at VIEWER and most *:manage scopes at EDITOR.
Two are capped higher, and the table below says so on each: workspace:manage
and billing:read both require ADMIN.
| Scope | Grants |
|---|---|
accounts:read | Read email accounts, DNS, and overviews — and list connected provider workspaces, preview their mailboxes, and read sync status. |
accounts:write | Connect, disconnect, and delete email accounts — and register a Google Workspace or Microsoft 365 tenant and start a mailbox sync. |
warmup:read | Read warmup settings. |
warmup:manage | Enable, pause, resume warmup, and update warmup settings. |
tags:read | List tags. |
tags:write | Create tags and set an account's tags. |
analytics:read | Read analytics overview, trends, leaderboard, deliverability, and per-account stats. |
notifications:read | List notifications. |
notifications:manage | Mark notifications read (single and all). |
billing:read | Read the organization plan, usage summary, and the caller's resolved capabilities. Requires an ADMIN role — billing is organization-level commercial data, so it is the one read scope capped above VIEWER. |
workspace:read | Read the workspace and its member roster. |
workspace:manage | Rename the workspace, and invite, re-role, or remove its members. Requires an ADMIN role — it is capped higher than the other *:manage scopes. It does not create or delete workspaces. |
Grant a key only the scopes it needs. Read-only integrations should never hold a
*:write or *:manage scope.
Revocation
Keys can be revoked from Settings → API keys in the web app. Revocation is
immediate — the next request made with a revoked key returns Api.InvalidKey
(401). There is no grace period.
Rate limits
The Public API applies two independent limits to every request. A request must pass both to be served.
| Layer | Counted against | Budget |
|---|---|---|
| 1 — API key | Your API key, split into three buckets | Set by your plan tier |
| 2 — IP address | The calling IP address | 100 requests/minute, flat |
Layer 1 is what your plan buys. Layer 2 is a fixed infrastructure ceiling — it is the same for every customer and no plan raises it.
Both use a fixed 60-second window that resets on the wall clock minute, not 60 seconds after your first call.
Buckets
The per-key layer counts requests into three separate buckets:
| Bucket | Applies to |
|---|---|
read | Read endpoints (GET). |
write | Mutating endpoints (POST, PUT, PATCH, DELETE). |
bulk | Bulk / batch operations. |
Each bucket has its own counter, so heavy reads do not consume your write budget.
Bulk endpoints are counted separately rather than just more cheaply: a single
bulk call can act on as many as 50 accounts, so it is not one unit of work. A
bulk route is charged to bulk regardless of its HTTP verb — POST …/accounts/bulk/tags and the read-shaped POST …/accounts/bulk-status both
draw on bulk, not on write or read.
Plan tiers
Per-key budgets, in requests per minute:
| Plan | read | write | bulk |
|---|---|---|---|
| Free | 5 | 2 | 1 |
| Trial | 20 | 10 | 4 |
| Starter | 10 | 5 | 2 |
| Growth | 20 | 10 | 4 |
| Scale | 30 | 15 | 6 |
| Scale +5k | 40 | 20 | 8 |
| Scale +10k | 45 | 22 | 9 |
| Scale +20k | 50 | 25 | 10 |
Billing cycle does not affect these numbers — monthly, quarterly, and yearly plans on the same tier share one budget.
The full-access trial is deliberately more generous than Starter so the API can be evaluated properly during it. If a workspace has no resolvable plan, the Free budget applies.
The IP ceiling
Every request also counts against 100 requests/minute per IP address, across all three buckets and all API keys.
Because the ceiling is higher than the largest per-key budget, a single key can never reach it on its own. It binds only when several keys call from the same address — so two things are worth knowing:
- Multiple API keys behind one server, or many workspaces automated from one machine, draw on one shared 100/min budget.
- If you integrate through a hosted platform (Zapier, Make, n8n) or from behind a corporate or cloud NAT gateway, you may share an outbound IP with other TrulyInbox customers and therefore share this ceiling. If you expect sustained volume, call from an address you control.
Response headers
Every response — including a rejected one — carries the state of both layers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your plan's limit for this request's bucket. |
X-RateLimit-Remaining | Requests left in that bucket this window. |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets. |
X-RateLimit-IP-Limit | The flat per-IP ceiling (100). |
X-RateLimit-IP-Remaining | Requests left against your IP this window. |
Retry-After | Seconds to wait. Present on 429 responses only. |
The unprefixed X-RateLimit-* headers always describe the per-key layer, so
they stay comparable to your plan. Watch X-RateLimit-IP-Remaining separately if
you call from a shared address.
Use X-RateLimit-Remaining to pace your client and back off as it approaches
zero. Read the limits from
GET /v2/workspaces/{workspaceId}/rate-limit rather
than hard-coding them — that endpoint reports the budget actually being enforced
for your key, alongside current consumption.
Exceeding a limit
Either layer rejects with HTTP 429 and the standard envelope, carrying
Api.RateLimitExceeded in errorCode:
{
"success": false,
"data": null,
"errorCode": "150005",
"errorMessage": "Read endpoint limit reached. Resets in 34 seconds."
}errorMessage names which limit you hit — the bucket (Read / Write / Bulk)
for the per-key layer, or explicitly the IP address for the ceiling. When a
request breaches both, the plan message is returned, since that is the one
you can act on.
The X-RateLimit-* headers are present on the 429 as well, and Retry-After
gives the seconds until the window rolls. Honour it rather than retrying
immediately; it is never less than 1.
Note that a rejected request still consumes budget in both layers, so retrying inside the same window makes matters worse — wait for the reset.
Failed authentication
Separately from the above, repeated failed API-key attempts from one IP are
locked out: 5 failures trigger a 15-minute cooldown, returning 429 with
Api.TooManyFailedAttempts and a Retry-After. A missing key does not count as
a failure; an invalid or revoked one does.
Errors
The response envelope
Every Public API response — success or failure — uses the same envelope:
{
"success": true,
"data": { },
"errorCode": null,
"errorMessage": null
}| Field | Type | Notes |
|---|---|---|
success | boolean | true on success, false on error. |
data | object | array | null | The resource on success; null on error. |
errorCode | string | null | A 6-digit code on error (see below); null on success. |
errorMessage | string | null | A human-readable message on error; null on success. |
The per-endpoint reference documents the shape of
datafor each operation. The envelope wrapping it is documented here, once — it is identical for every endpoint.
Error codes
Errors carry a stable, 6-digit errorCode. Branch on errorCode, never on
errorMessage — messages are for humans and may change; codes are the contract.
The first two digits identify the feature namespace. Public-API authentication,
authorization, and rate-limiting errors live in the Api (feature 15)
namespace. Every /v2 request passes through the guard stack
(ApiKeyGuard → ScopesGuard → WorkspaceRoleGuard → RateLimitGuard), so these are
the codes you will see most:
| Code | HTTP | Meaning |
|---|---|---|
150001 | 401 | API key is required — no Authorization: Bearer header was sent. |
150002 | 401 | Invalid or revoked API key. |
150003 | 403 | The key lacks the scope this endpoint requires. |
150004 | 403 | The key is not authorized for the :workspaceId in the path. |
150005 | 429 | Rate limit exceeded for this bucket. See Rate limits. |
150006 | 429 | Too many failed authentication attempts — temporary cooldown (per IP). |
150007 | 403 | Your plan doesn't include API access. The key is still valid and starts working again after an upgrade. |
040006 | 403 | Your subscription is paused, so the organization is read-only. Applies to every /v2 call, reads included — see below. The key is still valid and starts working again the moment the subscription resumes. |
A paused subscription blocks reads too. While your subscription is paused,
every /v2 endpoint returns 040006 — GETs as well as writes. A paused
organization changes nothing and sends nothing, so continuing to serve reads
would hand you data that is quietly frozen; the error is the signal.
It does not count against the failed-authentication cooldown and spends no rate-limit token, so a client that retries on a schedule won't lock itself out. Resume the subscription and the same key works again, unchanged.
Endpoints may also return feature-specific error codes from the domain they touch (for example, an unknown account id). Those codes follow the same 6-digit shape and the same envelope; handle them by code.
Key-management codes
Creating, listing, and revoking keys happens on the internal (session-authenticated)
/api-keys endpoints — the same surface the web app's API-keys settings screen uses.
Those operations return their own codes within the Api (15) namespace:
| Code | HTTP | Meaning |
|---|---|---|
150101 | 400 | A workspace id is required to create a workspace (ti_ws_) key. |
150102 | 403 | You can't create a key with a role higher than your own. |
150103 | 403 | You don't have access to the workspace for this key. |
150104 | 404 | No key with that id exists in your organization. |
Example error response
{
"success": false,
"data": null,
"errorCode": "150003",
"errorMessage": "Your API key doesn't have the required scope for this action."
}