Every endpoint follows the same rules. Learn them once and the whole API is predictable.
Authentication
Send Authorization: Bearer sk_test_... (test) or sk_live_... (live) on every /v1 request;
anything else is 401 auth.unauthorized. A revoked key is 401 auth.key_revoked. Manage keys via
API keys.
Test vs live
The key’s prefix decides the mode, and the mode scopes all data: a sk_test_ key never sees
live resources and vice-versa. Every resource carries livemode. Test mode uses the simulated
stamping service and tolerates the sample CSD; see Testing.
Money and time
Money is always a decimal string ("116.00") — never a float — so cents never drift. Amounts
are computed and rounded half-up at the currency’s decimal places. Times are ISO-8601 UTC
(2026-07-10T18:25:43Z). Ids are prefixed, time-ordered, base32 ULID-style: org_, key_,
fp_, inv_, dl_, we_, evt_, whd_, req_.
List endpoints take ?limit= (1–100, default 20) plus one cursor:
?starting_after=<id> — the next (older) page
?ending_before=<id> — the previous (newer) page
Sending both is 422 request.invalid_pagination. Responses are newest-first:
{ "object": "list", "url": "/v1/invoices", "data": [ ... ], "has_more": true }
Idempotency
Send Idempotency-Key: <unique> on any POST. The first response is stored for 24h and replayed
byte-for-byte for identical retries (replays add idempotent-replayed: true). A different body
under the same key is 409 idempotency.key_reuse; an in-flight duplicate is
409 idempotency.key_processing. This makes retrying a 502 safe — no duplicate stamp.
Fiscal profiles, invoices, webhook endpoints, and connected accounts accept a metadata object of
string→string (≤50 keys, key ≤40 chars, value ≤500). It round-trips untouched and always appears
(default {}). On PATCH, metadata is merged: send a key to set it, or a key with an
empty-string value to delete it.
{ "metadata": { "order_id": "ord_1789", "obsolete_key": "" } }
Expand
Where documented, ?expand=<field> inlines a related resource. Invoices support
?expand=fiscal_profile. An unsupported value is 422 request.invalid_expand.
Versioning
Pin a dated release with the Invoice-Version: 2026-07-15 header (Stripe-style). Omit it to use
the current version. An unknown value is 400 request.invalid_version. A key may also carry a
pinned version. Precedence: request header → key pin → current. The resolved version is echoed
on every response’s Invoice-Version header.
Versioning is real, not cosmetic — each dated version pins a response shape, so an old
integration keeps exactly the contract it was built against. Discover every version at
GET /v1/versions (public, no auth) and read the diff in the
Changelog. A version scheduled for removal is served with RFC 8594
Deprecation: true and a Sunset HTTP-date on every response.
Rate limits
A token-bucket limiter admits a steady rate with a burst. Authed responses carry RateLimit-Limit,
RateLimit-Remaining, and RateLimit-Reset. A throttled request is 429 rate_limit.exceeded with
a Retry-After header — back off and retry.
Request-Id
Every response carries a Request-Id header; it is also the request_id in every error body.
Log the Request-Id. Support and idempotency both key off it.
Usage
Every metered action is recorded on a read-only stream. GET /v1/usage returns
cursor-paginated usage_record objects for the acting org + mode — the Stripe usage-record analog.
Today a fresh CFDI stamp emits one record (kind: "stamp", quantity: 1, resource_id the
inv_…); idempotent replays and duplicate re-stamps never double-count. Filter with ?kind=,
?date_from=, and ?date_to= (inclusive bounds on created_at).
{ "object": "usage_record", "id": "ur_…", "kind": "stamp", "quantity": 1,
"resource_id": "inv_…", "livemode": false, "created_at": "2026-07-10T18:25:43Z" }
Errors
Errors are application/problem+json (RFC 9457) with a stable code, a human- and agent-readable
remediation, an optional pac object, request_id, and a doc_url deep-link. Switch on code,
show remediation. The full catalog lives in the Error reference.