Docs

Changelog

Dated record of every change to the Public API, and the 90-day notice period for breaking changes.

Every change to the /v2 API is recorded here, newest first. Dates are the day the change reached the API, not the day it was written up.

Each entry is labelled with what it does to your integration:

LabelWhat it means for you
AddedNew endpoint, scope, response field, or accepted value. Safe — nothing you already send or read changes.
ChangedExisting behaviour changed in a backward-compatible way. Worth reading; no action required.
DeprecatedStill works, and now carries a removal date. Migrate before that date.
RemovedGone. Only ever appears at least 90 days after the matching Deprecated entry.
FixedBehaviour that did not match this documentation now does.

Breaking-change policy

A breaking change is announced here at least 90 days before it takes effect. The Deprecated entry names both the removal date and what to use instead; the endpoint keeps working normally for the whole notice period.

What we treat as breaking

  • Removing an endpoint, a response field, or a query parameter.
  • Renaming anything in a request or response.
  • Making a previously optional request field required, or narrowing what an existing field accepts.
  • Changing the type of a response field.
  • Changing the meaning of an existing value.

What we do not treat as breaking

  • Adding an endpoint.
  • Adding a field to a response.
  • Adding an optional query parameter or request field.
  • Adding a new value to an enum in a response.
  • Adding a new scope.

That fifth one is the one that bites integrations, so it is worth stating plainly: a response enum can gain values without notice. On 2026-08-03 the account status vocabulary gained disconnected, and it will gain more as the product grows. Parse defensively — treat an unrecognised value as "some state I do not handle yet" rather than throwing. A client that switches exhaustively over these values will break on an addition we do not consider breaking.

How you will hear about it

Deprecations appear in this changelog with their removal date. Check it before you build against an endpoint, and after any integration failure you cannot explain.


2026-09-28

Added — senderEsp on warmup activity items

GET /v2/workspaces/{workspaceId}/accounts/{id}/warmup/activity now returns senderEsp on every item: google and microsoft for the Google and Microsoft families, other for plain SMTP, otherwise the lower-cased brand (zoho, yahoo, …).

Deprecated — provider on warmup activity items

provider is now an alias of senderEsp and carries the same value. Note the value format changed with it: it was the raw provider name (GMAIL, OUTLOOK); it is now the senderEsp value above. provider will be removed on 2026-12-27. Read senderEsp instead.

2026-09-22

Changed — mailbox checks now carry distinct error codes

POST /accounts/{id}/warmup/enable (scope warmup:manage) can refuse a mailbox because of our send-and-receive check. Those two refusals used to return the generic conflict and invalid_request, so telling them apart meant reading message. They now have codes of their own:

CodeStatusMeans
mailbox_verification_pending409The check is still running. Nothing to fix. Warmup starts by itself when it passes, so stop retrying.
mailbox_verification_failed400The mailbox failed the check. Fix the mailbox itself, then try again.

This is an enum gaining values, which is non-breaking per the policy above. A client that branched on conflict or invalid_request for these cases needs updating.

Fixed — partner-connected mailboxes start warming on their own

POST /accounts/oauth was documented as "warmup is not started; call warmup/enable". In fact a mailbox connected this way starts warming by itself once its DNS records and the send-and-receive check pass, within your plan's warming-mailbox cap. The documentation now says so. You don't need to call warmup/enable; calling it while the check is still running returns 409 mailbox_verification_pending.

2026-09-21

Removed — warmupLevel on warmup settings

MethodPath (under /v2/workspaces/{workspaceId})Scope
GET/accounts/{id}/warmup/settingswarmup:read
PATCH/accounts/{id}/warmup/settingswarmup:manage

Warmup levels (NOVICE … GRANDMASTER) no longer exist. The GET response no longer carries warmupLevel, and a PATCH that sends it is rejected with 400 invalid_request, since unknown fields are refused. To control volume, send warmupMaxLimit instead (see Added below).

This removal is immediate and has no 90-day notice period. It is an exception to the breaking-change policy above: there is no matching Deprecated entry. If your integration sets or reads warmupLevel, move it to warmupMaxLimit.

Added — warmupMaxLimit and warmupCurrentLimit on warmup settings

PATCH /accounts/{id}/warmup/settings accepts warmupMaxLimit, the most warmup emails the mailbox may send in a day. Warmup still starts low and climbs toward this number as the mailbox's reputation allows. It must be an integer of at least 5. A value below 5 returns 400 invalid_request. A value above your plan's per-mailbox cap (maxWarmupPerAccount on the GET response) returns 400 invalid_request with a message that names the cap. When maxWarmupPerAccount is null, there is no upper limit.

GET /accounts/{id}/warmup/settings now returns warmupCurrentLimit: the daily sending limit in force today, somewhere between warmupMinLimit and warmupMaxLimit.

A lower warmupMaxLimit takes effect at the mailbox's next local midnight. A plan change that leaves a mailbox's max above the new plan's cap lowers the max to the cap, and sending drops to it immediately.

Changed — warmup-settings fields report what is in force

These fields on GET /accounts/{id}/warmup/settings keep their names, but their meaning has changed. The null on maxWarmupPerAccount and the new meaning of the band fields count as breaking under the policy above. Like the removal, they take effect immediately with no notice period.

  • maxWarmupPerAccount can now be null, which means the plan has no per-mailbox cap. It used to return a number even for an unlimited plan. Treat null as unlimited, not as zero.
  • warmupMinLimit is fixed at 5 for every mailbox and can't be changed.
  • replyRateMin / replyRateMax and threadTendencyMin / threadTendencyMax are the bands currently in force. In auto mode (replyRateMode LEVEL, replyRateCustom: false) they follow the mailbox's current daily limit, and move up and down with it. A custom reply rate is still reported as its fixed value.
  • replyRateMode: "LEVEL" keeps its wire value and now means "auto": sending it switches a custom reply rate back to the band for the current daily limit.
  • replyRate on account responses (GET /accounts, GET /accounts/{id}) is the custom value, or in auto mode the top of the band for the current daily limit.

The maxLimitMin / maxLimitMax filters on GET /accounts now accept values up to 100 (was 40).

2026-09-11

Fixed — new workspaces are shared with every owner

POST /v2/workspaces now makes every owner of your organization, the key's creator included, an owner of the workspace it creates. Before this fix the creator was added as an admin only and other owners were not added at all, so a workspace provisioned over the API was invisible to the rest of your owners. Workspaces already created that way are repaired in the same release. Nothing changes in what you send or read.

2026-09-10

Added — workspace budgets

MethodPathScope
GET/v2/workspaces/quotasworkspace:quota
PATCH/v2/workspaces/quotasworkspace:quota

Read and reallocate each workspace's daily warmup budget — its share of your organization's total daily cap. GET returns the cap (orgLimit; null means unlimited, not zero), how much is allocated, and every workspace's dailyWarmupBudget. PATCH takes { quotas: [{ workspaceId, dailyWarmupBudget }] } and changes only the workspaces listed. The cap is checked once across the whole request before anything is written, so one call can move budget between workspaces even when the organization is fully allocated.

POST /v2/workspaces also accepts an optional dailyWarmupBudget, so a workspace can be created and funded in one call. Sending it requires workspace:quota in addition to workspace:provision; a budget over the cap is rejected with 400 and no workspace is created.

On either route, an allocation that would exceed the cap returns the new error code quota_exceeds_org_limit, not invalid_request: the request is valid, and the fix is to lower another workspace's budget or upgrade.

workspace:quota is a new scope, separate from workspace:provision so that budget control is never granted by accident to a key issued only to create workspaces. Like provisioning, it needs a personal (ti_pk_) key belonging to an organization owner; workspace keys are refused. The per-workspace PATCH /v2/workspaces/{workspaceId} still does not edit the budget, because a budget is carved out of the organization-wide cap.

Changed — GET /v2/workspaces accepts workspace keys

MethodPathKeyScopeReturns
GET/v2/workspacespersonal (ti_pk_), creator is an organization ownerworkspace:provisionevery workspace in the organization
GET/v2/workspacesworkspace (ti_ws_)workspace:readonly the workspace the key is bound to

A workspace key used to be refused on this route whatever its scopes. It is now accepted and gets back a single-element array holding its own workspace — the same shape a personal key gets, so you never branch on key type. role on that element is the role the key acts at. Nothing changes for personal keys, and POST /v2/workspaces still refuses workspace keys: creating a workspace is an organization-level action.

Changed — POST /accounts/oauth is documented as partner use only

Documentation only; behaviour is unchanged. The endpoint has always refused every call with 403 partner_not_configured unless the organization registered and verified a partner callback. It is now labelled partner use only so it is not mistaken for a general-purpose way to connect a mailbox — for that, use POST /accounts/oauth-url.

2026-09-08

Added — connect an OAuth mailbox headlessly

MethodPath (under /v2/workspaces/{workspaceId})Scope
POST/accounts/oauthaccounts:write

For partners who run their own OAuth consent screen and hold the mailbox's refresh token themselves. Send the provider, the emailAddress, a live accessToken and its expiresAt; the mailbox is connected without anyone opening a browser.

There is no refreshToken field, and sending one is rejected. That is the shape of the integration rather than an omission: you keep the refresh token, and TrulyInbox calls your refresh endpoint when the access token expires. Register that endpoint under Settings → API keys before your first call — without a verified registration this endpoint returns 403 partner_not_configured, because a mailbox connected this way could never be renewed.

The mailbox is identified by the token, not by your payload. We query the provider's own profile endpoint and reject a mismatch with 422 email_mismatch. So a wrong emailAddress fails loudly instead of attaching someone else's token to the row you named.

expiresAt must be more than five minutes out — a token expiring inside our early-refresh window would call your endpoint back immediately.

Warmup is not started. Call POST /accounts/{id}/warmup/enable once the DNS gate passes. One mailbox belongs to exactly one workspace: connecting an address that already exists elsewhere is rejected.

Added — create and list workspaces

MethodPathScope
GET/v2/workspacesworkspace:provision
POST/v2/workspacesworkspace:provision

Provision a workspace per end customer instead of asking someone to click through the panel. These are the only /v2 routes with no {workspaceId} segment — they are how you obtain one.

workspace:provision is a new scope, deliberately separate from workspace:manage: managing a workspace you were given is a different privilege from minting new ones. Both routes additionally require a personal (ti_pk_) key belonging to an organization owner. A workspace-scoped (ti_ws_) key is refused whatever its scopes, because such a key is pinned to one workspace by construction.

The key's creator becomes the admin member of every workspace it creates, so mint the key from a service account you control rather than a personal login — if that person leaves the organization, member management breaks across all of them.

Deleting a workspace over the API is not supported.

Changed — three 403s and a 422 now carry distinct error codes

Previously every 403 returned forbidden and every 422 returned unprocessable_entity, so telling apart "you are out of mailbox capacity" from "you are out of warmup capacity" meant reading message prose we ask you not to parse. Four conditions now have codes of their own:

CodeStatusMeans
plan_limit_reached403Connected-mailbox cap reached — upgrade or remove a mailbox.
warmup_limit_reached403Warming-mailbox cap reached — pause one, or upgrade.
partner_not_configured403No verified partner callback registered for this organization.
email_mismatch422The access token belongs to a different mailbox.

The remedies differ, which is why the codes now do. This is an enum gaining values — non-breaking per the policy above — but a client that branched on forbidden to detect a capacity problem needs updating, since these no longer report as forbidden.

2026-09-07

Added — connect a single Microsoft mailbox

MethodPath (under /v2/workspaces/{workspaceId})Scope
GET/accounts/microsoft/consent-urlaccounts:write

Connect one Microsoft mailbox without running your own OAuth app. Pass the email you want connected; you get back a link and its expiresAt. Send the link to the mailbox owner — the mailbox is connected when they complete consent in their browser, not by this call.

The link is single-use and expires in 30 minutes, and it is pinned to the address you named: if a different Microsoft account signs in, nothing is connected. That check is what confirms ownership, since the person clicking through is your customer rather than you. It compares against both identities Microsoft exposes for a mailbox, so an onmicrosoft.com sign-in still matches a custom-domain address on the same mailbox.

When the owner finishes they land on a result page carrying partnerConnectStatus=success|failed and, on failure, partnerConnectReason=missing_state|consent_denied|expired_or_invalid_link|email_mismatch|error. Treat that as a hint, not a receipt — poll GET /accounts to confirm. Warmup is enabled automatically on the connected mailbox, subject to your plan's capacity.

Connecting a mailbox that is already in this workspace re-authorizes it in place. One that is connected in a different workspace is rejected.

POST /accounts/oauth-url is unchanged and still covers Gmail, Google Workspace, Outlook and MS365 — use it when you do not need the mailbox pinned in advance.

GET /v2/workspaces/{workspaceId}/provider-workspaces/microsoft/consent-url now accepts an optional domain (the customer's Microsoft domain, or their tenant GUID). Two things change when you pass it:

  • The consent prompt is pinned to that tenant. Without it the admin consents for whichever Microsoft account they happen to be signed into, which need not be the tenant you meant.
  • The response gains tenantId, so you can go straight to POST /provider-workspaces instead of reading the GUID out of the redirect or asking the admin for it.

A domain with no Microsoft tenant behind it returns 422 rather than quietly falling back to the generic prompt. Omitting domain behaves exactly as before.

Resolving the tenant proves it exists — not that consent was granted. The POST still verifies the grant before anything is stored.

2026-08-10

Added — workspace settings and members

Manage the workspace itself and the people in it.

MethodPath (under /v2/workspaces/{workspaceId})Scope
GET`` (the base path itself)workspace:read
PATCH`` (the base path itself)workspace:manage
GET/membersworkspace:read
POST/members/inviteworkspace:manage
PATCH/members/{userId}workspace:manage
DELETE/members/{userId}workspace:manage

Two new scopes come with them: workspace:read and workspace:manage.

workspace:manage requires an Admin role in the workspace — a higher bar than the other *:manage scopes, which need only Editor. A key granted the scope but held by an Editor is rejected.

Creating and deleting workspaces is not part of this surface and stays in the web app. GET /members lists organization owners alongside members; they hold implicit admin on every workspace and are marked isOrgOwner: true.

2026-08-04

Added — billing

MethodPath (under /v2/workspaces/{workspaceId})Scope
GET/billing/summarybilling:read
GET/billing/capabilitiesbilling:read

Read the organization's plan, current usage against its limits, and the caller's resolved capabilities. New scope: billing:read. It is read-only and has no write counterpart — plan changes go through checkout in the web app.

2026-08-03

Added — account lifecycle, bulk operations, and provider workspaces

The largest expansion of the surface so far — 21 endpoints.

Account lifecycle

  • POST /accounts — connect an SMTP/IMAP mailbox.
  • POST /accounts/oauth-url — get a consent URL for Gmail, Google Workspace, Outlook, or Microsoft 365. OAuth mailboxes need a browser, so this returns a URL rather than connecting.
  • POST /accounts/{id}/disconnect — stop warmup, keep the data.
  • DELETE /accounts/{id} — permanent, returns 204.

Bulk operations — batched forms of the single-account routes, up to 50 ids per call, partial-succeed:

  • POST /accounts/bulk-status
  • POST /accounts/bulk/warmup/enable
  • POST /accounts/bulk/warmup/pause
  • POST /accounts/bulk/tags
  • POST /analytics/accounts/stats

Bulk endpoints draw on their own rate-limit bucket. See Rate limits.

Provider workspaces — connect a whole Google Workspace or Microsoft 365 tenant and import its mailboxes:

  • GET /provider-workspaces and POST /provider-workspaces
  • GET /provider-workspaces/microsoft/consent-url
  • GET /provider-workspaces/{providerWorkspaceId}/preview
  • POST /provider-workspaces/{providerWorkspaceId}/sync
  • GET /provider-workspaces/{providerWorkspaceId}/sync-status

Deliverability and diagnostics

  • GET /accounts/{id}/setup-score and POST /accounts/{id}/setup-score/refresh
  • GET /accounts/{id}/deliverability — fixed 30-day window.
  • GET /accounts/{id}/warmup/activity — paginated per-message send history.

Operational

  • GET /v2/health — unauthenticated liveness probe. The only endpoint outside the workspace-scoped path.
  • GET /rate-limit — your own remaining budget for all three buckets. Requires no scope: a key that cannot read its own budget cannot back off correctly.

Changed — disconnected account status

disconnected was added to the status filter on GET /accounts and to the status-counts object returned by the bulk status endpoint.

Additive, and therefore not a breaking change under the policy above — but see the note there about parsing enums defensively.

2026-06-19

Added — initial release

The first published /v2 surface, 21 endpoints across five domains:

DomainEndpoints
AccountsList and read accounts, DNS check, account overview.
WarmupRead warmup state and settings; update settings; enable, pause, and resume.
TagsList and create workspace tags; replace an account's tag set.
AnalyticsWorkspace overview, trends, leaderboard, deliverability, per-account stats.
NotificationsList; mark one read; mark all read.

Authentication is by API key as a bearer token, authorized by the scopes stamped on the key intersected with the key's role in the target workspace. See Authentication.


No endpoint, field, or accepted value has been removed since the initial release.