# Versioning

The API is versioned with **dated versions** so a breaking change never breaks your
integration without warning. You pin a version and upgrade on your own schedule.

---

## How it works

Every request resolves an effective version like this:

1. The **`API-Version`** request header, if it is a well-formed `YYYY-MM-DD`.
2. Otherwise the platform's **current version** (`2026-01-01`).

A version can be stored on the API key in the dashboard. That pin is **not consulted
today**. There is only one behavioural version, so the header (when well-formed) is echoed
and every other request gets the current default. The pin records intent for when a second
version exists; do not treat it as selecting a contract yet.

Every response echoes the resolved version in the **`API-Version`** response header, and a
unique **`Request-Id`** header for support correlation.

```bash
curl https://api.worthypayments.com/v1/charges \
  -H "x-api-key: sk_live_xxx" \
  -H "API-Version: 2026-01-01" \
  ..
# Response headers:
#   API-Version: 2026-01-01
#   Request-Id: req_8f2c..
```

The current version is **`2026-01-01`**. The same version stamps the `api_version` field on
every webhook event.

> **There is currently only one version.** `2026-01-01` is both the current version and
> the oldest one, so pinning does not select a different contract today. It records your
> intent for the first time we ship a second one. Everything below describes how pinning
> will behave then; right now every request gets the same behaviour whatever it pins.

---

## Pinning

When a second version exists, pin it on your key in the dashboard so all your requests
use a stable contract, and use the `API-Version` header to try a newer one first. Until
then the pin is stored but unused; send the header only if you want that date echoed
back.

### The header is checked for shape, not for existence

`API-Version` is validated as a `YYYY-MM-DD` date and nothing more. A **well-formed but
unknown** date is accepted and echoed straight back:

```bash
curl ... -H "API-Version: 1999-01-01"
# Response headers:
#   API-Version: 1999-01-01     ← reflected, not honoured
```

So do not treat the response header as confirmation that a version exists. It tells you
what you asked for, not what you got. A malformed value (`v2`, `2026-1-1`) is ignored
entirely and the response carries the resolved default instead, also silently, since an
unparseable header is never a hard failure.

The practical rule: pin the version you read in these docs. Getting it back in the response
header proves the request reached us, not that the date is a real version.

## What counts as a breaking change

New fields, new endpoints, new event types, and new enum values are **additive** and ship
without a version bump, so write your integration to ignore unknown fields. A version bump is
reserved for changes that could break a correctly-written integration (a removed field, a
changed type, a renamed value).

## Request ids

Include the `Request-Id` from a response when you contact support. It lets us find the
exact request in our logs. Every response and every error carries one (the error envelope
also includes `request_id`).

## Object ids are opaque

**Never pattern-match, parse, or validate the shape of an id.** Store the exact string we
return and send it back verbatim. Formats differ per object and are not part of the
versioned contract. A check that a charge id "starts with `txn_`" is a bug waiting for the
first id that does not.

The shapes are genuinely mixed today, which is the point:

| Object | What the API returns |
|---|---|
| Charge | `TXN-` + a UUID (`TXN-8ac4796a-…`). Uppercase, hyphen, **not** `txn_`. |
| Refund | `re_` + the charge-style id (`re_TXN-…`). |
| Payment intent | `pi_` + 32 hex characters. The `client_secret` is that id + `_secret_` + an opaque token. |
| Event | `evt_` + a UUID. |
| Customer | `cus_` + a UUID (`cus_8ac4796a-…`): the customer's `id`, and the customer on payment intents, subscriptions, invoices, customer sessions and customer state. The bare UUID is what charges carry as `customer`, pre-dispute alerts as `customer_id`, and webhook payloads as `customerId`; a saved card's `customer` repeats the id you sent in the path. Customer-id inputs accept either form. `CUS-<serial>` is an accepted **lookup slug**, not the id we return. |
| Merchant account | an opaque account id, no prefix. |
| Checkout link, product, blocklist entry, fraud check | bare UUIDs, no prefix. |

Cursors are opaque too, and are **not** ids. See [Pagination](./13-pagination.md). Treat
`next_cursor` as a token to hand back, never as an object reference.
