Version lives in the path. Every URL on this site starts /v1/, and a /v1/ call written today will keep working the way it works today.
Adding a field is not a breaking change, so parse defensively: a new key in a response object should not upset your client. Removing a field, renaming one, changing a type, or narrowing an accepted value is breaking, and those go in a new version rather than into /v1/.
One category sits awkwardly between the two. Adding a new reason value inside an existing result is additive to us and can break a client that switched exhaustively on the old set. Branch on result, treat a reason you don't recognize as extra detail, and you are safe.
Reference
| Change | Breaking? | How it ships |
|---|---|---|
| New field in a response | No | Any release. Ignore what you do not read. |
| New optional request field | No | Any release. |
| New value in an enum | No, by our definition | Announced in the changelog. Handle the default case. |
| Field removed or renamed | Yes | New version. /v1/ keeps the old shape. |
| Type of a field changes | Yes | New version. |
| Endpoint removed | Yes | New version, with a year of overlap. |
A client that still works next year
Two small habits decide whether an additive change breaks you. The first is your JSON parser. Some typed languages ship parsers that fail on any field they weren't told about. A client built that way breaks on the day we add one. Set yours to ignore unknown fields.
Your switch statements are the second. Say a release adds a reason value your code has never seen. If you switch on reason with no default branch, that row falls through or throws. Branch on result, keep a default in every switch you write, and treat a strange reason as plain text for your logs and nothing more.
Give result a default branch too, even though the four results are settled. The safest default is to treat a verdict you don't know the way you treat unknown. Keep the contact, hold it out of the main send, and tell a person so they can update the client.
Keeping the provisional fields in one place
Several names across these docs are marked provisional. They are the ZapBounce-Version header, the encoding option on uploads, the idempotent_replay flag and the filter parameter on results. Any of them may be renamed before v1 is called stable.
You can make that cheap. Put every read of a provisional field inside one small module in your client, so a rename is a one-line change. Reading idempotent_replay in six files means six edits when it moves. The reason list is settled, but it can still grow. Don't store it in a database enum type, since a new value would then need a migration before your code could save the row.
Leave the version header alone for now, because nothing depends on it yet. For the settled fields, relax. result, smtp_code, billed, batch_id and the summary counts are the core of the contract. A rename there would ship as a new version with a year of overlap.
Questions developers ask
Will /v1/ break?
Not by removal or rename. New fields and new enum values will appear, which is why your client should ignore what it does not recognize.
How much notice before a version retires?
A year of overlap, announced when the successor ships.
How do I know what changed?
The changelog page, which records additive changes too.