Versioning

View as Markdown

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:

ObjectWhat the API returns
ChargeTXN- + a UUID (TXN-8ac4796a-…). Uppercase, hyphen, not txn_.
Refundre_ + the charge-style id (re_TXN-…).
Payment intentpi_ + 32 hex characters. The client_secret is that id + _secret_ + an opaque token.
Eventevt_ + a UUID.
Customercus_ + 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 accountan opaque account id, no prefix.
Checkout link, product, blocklist entry, fraud checkbare 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.