Tone docs

Versioning and breaking changes

/v1 is a promise, not a version number. What that commits us to, what it asks of you, and how it is enforced.

There is no plan for a /v2.

That is not optimism β€” it is a constraint we accepted, and it shapes how the API is designed. Anything we cannot do additively, we do not do.

What we promise

Additive only. These can appear at any time and are not breaking:

  • A new endpoint
  • A new optional request field
  • A new response field
  • A new value in any enum-shaped field
  • A new webhook event type
  • A clearer error message

Never, on /v1:

  • Removing or renaming an endpoint, a field, or an error code
  • Changing a field's type
  • Making an optional request field required
  • Removing a value we have previously published
  • Changing an endpoint's success status code
  • Requiring a broader scope than before

What that asks of you

One thing, and it is the whole bargain.

πŸ”΄ Treat every enum as an open set. A field documenting answered, no_answer, busy, voicemail, failed, unknown is telling you what exists today β€” not what can ever arrive. If your client throws on an unrecognised value, our compatible change becomes your outage.

// Wrong β€” turns our additive change into your incident
switch (call.disposition) {
  case 'answered': return handleAnswered(call);
  case 'busy':     return handleBusy(call);
  default:         throw new Error(`unknown disposition: ${call.disposition}`);
}

// Right
switch (call.disposition) {
  case 'answered': return handleAnswered(call);
  case 'busy':     return handleBusy(call);
  default:         return handleUnknown(call);   // log it, carry on
}

The same applies to error.code: branch on the codes you handle, and fall back on error.type and the HTTP status for one you have not seen.

How this is enforced

Not by review. backend/openapi.public.json is committed, and every change to the API is diffed against it:

pnpm --filter backend openapi:check   # the spec matches the code
pnpm --filter backend openapi:gate    # the change is allowed on /v1

The gate classifies every difference and fails the build on a removed operation, a removed or retyped field, a narrowed set of known values, a newly required field, a moved success status or a widened scope. A breaking change cannot be merged by someone who did not notice it was breaking.

Deprecation

When something is superseded we mark it deprecated, keep it working, and say what to use instead. We do not set removal dates for /v1, because a removal date is a breaking change with a delay on it.

A voice retired by our speech provider is the clearest case: it disappears from the pickers, keeps working for every agent already using it, and is flagged deprecated in the catalog. A vendor's documentation edit must not break a tenant that changed nothing.

Date-pinned versions

Some APIs let you pin a version by date. We have deliberately not built that.

A pinning mechanism makes breaking changes possible, and a thing that is possible gets justified. The constraint is more valuable than the escape hatch β€” so if we ever need one, that will be a considered decision with a reason, not a header that was already there.

Was this page helpful?

On this page