Versioning
View as MarkdownThe 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:
- The
API-Versionrequest header, if it is a well-formedYYYY-MM-DD. - 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.
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-01is 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:
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. Treat
next_cursor as a token to hand back, never as an object reference.
