Skip to main content
POST
Create and stamp a CFDI 4.0 invoice via the configured PAC
Builds, validates, and stamps (timbra) a CFDI 4.0 invoice through the configured PAC (Proveedor Autorizado de Certificación) in a single call. On success, the invoice is fiscally valid at the SAT and is recorded in the organization’s invoice history.
This endpoint takes no organizationId in the URL. Authenticate with either a Supabase Bearer session (send the X-Organization-Id: <org-id> header — the user must be a member of that organization) or an API key (the organization resolves automatically from the key; any X-Organization-Id header sent alongside an API key is ignored).

Authentication

Accepts either:
  • A Supabase Bearer session (Authorization: Bearer <token>) with the X-Organization-Id: <org-id> header — the authenticated user must be a member of that organization.
  • An API key (X-API-Key: sk_...) with the write:invoices scope — the organization is resolved from the key itself.
The issuer (RFC, legal name, régimen fiscal, ZIP) is resolved from the organization’s fiscal profile (PUT /organizations/:organizationId/legal) — configure that before creating your first invoice, or this endpoint returns a 400.

Headers

string
Canal desde el que se crea el CFDI: web, api, sdk, cli o mcp. Es opcional; si no se envía o el valor no es válido, se registra api. El SDK oficial lo envía como sdk (configurable con la opción client) y el servidor MCP como mcp. Sirve para métricas y no afecta el timbrado.

Request Body

* Exactly one of customer or customerId is required — sending both, or neither, returns a 400. ** Required when type is E (Egreso/nota de crédito) — must be the UUID fiscal of an existing, vigente Ingreso CFDI belonging to the same organization; that invoice is the one being credited. Ignored for type: "I" and type: "T". If the related invoice is PPD, the credit note reduces its balance, so it must use the invoice’s currency, its total cannot exceed the invoice’s pending balance, and its taxes must be a subset of the invoice’s (same tax, factor type and rate, never more base than the invoice has left); otherwise the request returns 422 and nothing is stamped. A credit note for a PPD invoice that is cancelled or being cancelled returns 409 (“La factura está cancelada o en proceso de cancelación”). While another credit note for the same PPD invoice is being stamped, the request returns 409 (“La factura tiene una nota de crédito en timbrado; reintenta en unos minutos”); a credit note and a payment sent at the same time never exceed the balance together. *** Required for type: "I" and type: "E". Not required — and discarded if sent — for type: "T" (Traslado), which carries no forma de pago. subtotal and total are not request fields — they’re computed automatically from items.
date must be the current wall-clock time in Mexico City (America/Mexico_City), not UTC. The SAT only accepts an issuance date within roughly the last 72 hours (never in the future) and the PAC compares your date value directly against its own Mexico City server clock, with no timezone conversion. Sending a UTC timestamp (e.g. new Date().toISOString()) looks ~6 hours in the future from that reference and gets rejected — and confusingly, that rejection is often reported back as CFDI40102 (“digest doesn’t match seal”) instead of a clear date error. If you ever see CFDI40102, check this before suspecting your CSD certificate.
Node.js
cURL

customer (inline)

items[]

Each line item can either reference an existing product/service by productId, or send the catalog fields inline (productKey, unitKey, description, unitPrice). Sending both, or neither, returns a 400. * Exactly one of productId or the inline catalog fields (productKey/unitKey/description/unitPrice) is required per item.

taxes[]

Business rules applied automatically

  • The issuer (RFC, legal name, régimen fiscal, ZIP) is resolved from your organization’s legal data (Update Legal Data), so you don’t send it on every request.
  • When an item uses productId, its description, unitPrice, productKey, unitKey, amount, taxObject and taxes are all resolved from the stored product — you only need to send quantity and productId. If the product has taxIncluded: true (only supported when its only transferred tax is IVA at a rate), the price is split so that unitPrice and amount exclude the transferred taxes (e.g. a $499.00 product with 16% VAT included becomes unitPrice 430.17, amount 430.17, VAT 68.83; unitPrice is always rounded to 2 decimals). An explicit taxObject on the item overrides the product’s taxability.
  • taxObject must be consistent with the item’s taxes: 02 requires at least one tax line; 01, 03 and 04 must not have tax lines. Inconsistent items are rejected with a 400 before stamping.
  • base is required on every tax line in CFDI 4.0 — filled from the item’s amount if omitted.
  • subtotal/total are computed from items[].amount and items[].taxes[].amount.
  • Invoices to público en general (customer.taxId = XAXX010101000, customer.legalName = PUBLICO EN GENERAL, type = I) automatically get the SAT InformacionGlobal node and use = S01.
  • Traslado (type = T) invoices are stamped without paymentForm/paymentMethod, with subtotal and total at 0, and with use = S01 — these are overridden server-side, per CFDI 4.0 (Anexo 20), regardless of what the request sends. Every item’s unitPrice, amount, and taxes[] are zeroed/dropped too (with taxObject = 01), since CFDI 4.0 requires subtotal to equal the sum of the item amounts; quantity, description, productKey, and unitKey are kept.
  • The customer’s RFC, régimen fiscal, and uso CFDI are validated against the SAT catalog compatibility matrix before stamping (see Create Customer for the same validation rules).

Substituting a CFDI

To replace a stamped Ingreso or Traslado that was issued with errors, issue a new CFDI that relates the original with TipoRelacion 04 (sustitución of previously issued CFDIs) via relatedCfdis, then cancel the original with motivo: "01". Phase 1 rule: only one relation, of type 04, to one UUID, and only on Ingreso (I) or Traslado (T). Egreso keeps using relatedInvoiceUuid. A Complemento de Pago is replaced with substitutes on Register Payment. The original is not required to have the same receptor or the same type as the new CFDI — fixing those is the usual reason to substitute. Each relatedCfdis item has a type (the SAT TipoRelacion code; only "04" is accepted for now) and a uuids array (exactly one UUID fiscal). The stamped CFDI’s relatedCfdis are returned in the invoice detail and list responses, and the original CFDI exposes the replacement under substitutedBy (uuid, serie, folio, status).

Series and folios

series and folioNumber are optional. See the Series and folios guide for the full picture.
  • Neither: Timbrix uses the organization’s default series for the CFDI type (I, E or T) and the next free folio of that series in the current environment.
  • series only: the series must exist, and the folio is the next free one. Otherwise the request returns 404 SERIES_NOT_FOUND.
  • Both: the folio is used exactly as sent and never moves the series counter.
  • The same series and folio can only be used once per organization and environment, including cancelled CFDI. A repeat returns 409 FOLIO_ALREADY_USED.
  • An idempotent replay (same idempotencyKey) returns the original CFDI and does not use another folio.
  • A stamp that fails still uses its automatic folio, so the next invoice may skip a number. The SAT does not require consecutive folios.

Idempotency

Stamping is not safe to blindly retry: if your request times out, you can’t tell whether the CFDI was stamped at the SAT. Pass a unique idempotencyKey (e.g. your own order ID) and retry with the same key:
  • If an invoice was already created with that key, it is returned instead of stamping a second one. A replayed request returns the CFDI’s current state, so status may be "cancelado" if it was cancelled after the first stamp.
  • If the first request’s outcome is still being confirmed, you get 409 PAC_STAMP_PENDING. Retry later with the same key.
  • If the first request was confirmed as not stamped, the retry stamps normally.
  • A retry must send the same body. The same key with different data (another amount, customer, series…) returns 409: “La idempotencyKey ya se usó con otros datos. Si es otro CFDI, usa una llave distinta”. The order of the JSON fields does not matter, but the fields do: send the same ones with the same values (an omitted field and the same field with its default value, such as currency and "currency": "MXN", count as different).
  • Keys are per environment. A key already used in production returns 409 from a sandbox key (and the other way around), without returning the CFDI: “La idempotencyKey ya se usó en producción; usa una llave distinta en sandbox”.
Without an idempotencyKey, every request stamps a new CFDI — retries are your responsibility to avoid.

Example Request

cURL
TypeScript SDK
With an API key, omit the X-Organization-Id header (and the SDK’s organizationId argument) entirely — it’s resolved from the key:
TypeScript SDK (API key)
Using inline concept data instead of productId:
cURL
Issuing an Egreso (type: "E") — a credit note against a previously stamped Ingreso, referenced via relatedInvoiceUuid. The tax line below uses withholding: true to record a retención instead of a traslado:
cURL
Substituting a CFDI — issue the corrected Ingreso with relatedCfdis pointing at the UUID of the CFDI being replaced (the original keeps being vigente until you cancel it with motivo: "01"):
cURL
Issuing a Traslado (type: "T") — a comprobante that moves goods without transferring ownership, so there’s no relatedInvoiceUuid. A Traslado is not a sale: per CFDI 4.0 (Anexo 20) it carries no paymentForm/paymentMethod, its subtotal and total are stamped as 0, and use is forced to S01 (“Sin efectos fiscales”) server-side no matter what you send:
cURL
Item amount/unitPrice are still accepted on a Traslado (the example above sends amount: 500), but because the movement has no monetary value they are stamped as 0 along with any taxes[] — so don’t be surprised when the submitted amounts don’t appear in the resulting CFDI. Send them or omit them; the stamped result is the same.

Example Response

object[]
Present only when there are 69-B warnings; the invoice is stamped anyway. Each item has code, rfc, role (receptor) and a human-readable message. code is RFC_69B_PRESUNTO when the receptor’s RFC appears as Presunto in the SAT’s 69-B list, or RFC_69B_DEFINITIVO when it appears as Definitivo but this CFDI corrects or complements one already stamped to that same RFC (a credit note with relatedInvoiceUuid, or a substitution with relatedCfdis type 04). A retry with the same idempotencyKey returns the original response without new warnings. See Lista 69-B del SAT.
string | null
Canal de origen registrado (web, api, sdk, cli, mcp). null para CFDI creados antes de que existiera este campo.

Common Errors

400 Bad Request

Invalid or incomplete invoice payload, missing X-Organization-Id header on a session-authenticated request, an issuing organization without complete fiscal data, sending both/neither customer and customerId, sending both/neither productId and the inline concept fields on an item, or a customer whose RFC/régimen/uso CFDI combination fails SAT catalog validation.
A 400 can also carry a SAT catalog error embedded in message (e.g. "Error timbrado: CFDI40143 - Este RFC del receptor no existe..."). If you see CFDI40102 - El resultado de la digestión debe ser igual al resultado de la desencripción del sello, check the date field first (see the warning above) — it is frequently caused by sending it in UTC rather than Mexico City time, not by an actual certificate problem.

401 Unauthorized

Missing or invalid Bearer token / API key.

402 Payment Required — SUBSCRIPTION_INACTIVE

Production only: stamping in production requires an active paid subscription, so nothing is stamped, even within the monthly quota. reason says why:
  • trial_active: the free trial stamps in sandbox only.
  • trial_ended: the trial ended without a plan.
  • payment_pending: a subscription payment failed and has not been settled.
  • subscription_ended: the subscription was cancelled or refunded.
  • no_subscription: no plan was ever contracted.
Sandbox keeps working, and the CFDI you already issued can still be listed, downloaded and cancelled. Cancelling with reason 01 needs a replacement CFDI stamped in Timbrix, so without a plan use 02, 03 or 04. If a payment is pending and you collected PPD invoices, keep in mind that the Complemento de Pago must be issued by the 5th day of the following month. Choose a plan in Settings → Billing; stamping is re-enabled as soon as the payment is confirmed. This check runs before the CSD check, so PRODUCTION_NOT_READY only reports missing CSD steps.

402 Payment Required — RFC_LIMIT_EXCEEDED

Production only: the organization is outside the RFC slots of your plan (Starter 1, Pro 3, Business 10, Enterprise unlimited). Your plan covers a limited number of RFC (organizations) per account, so nothing is stamped. details.limit is the number of RFC your plan includes. Sandbox is not limited. Upgrade your plan or close an organization to free a slot.

403 Forbidden

The authenticated user is not a member of the organization sent in X-Organization-Id, or the API key does not have the write:invoices scope.

404 Not Found

customerId does not exist, productId does not exist, or relatedCfdis points to a UUID that is not an invoice of your organization (see Substituting a CFDI) — in either case, belonging to a different organization than the authenticated one counts as not found.

404 Not Found — SERIES_NOT_FOUND

You sent series without folioNumber and that series does not exist in the organization. Create it with Create series or omit series to use the default.

409 Conflict — FOLIO_ALREADY_USED

That series and folio already belong to another CFDI of the organization in this environment (a cancelled CFDI counts). Omit folioNumber to get the next free one, or send a different folio.

409 Conflict — PAC_STAMP_UNCONFIRMED

The request may have reached the PAC, but Timbrix could not confirm right away whether the CFDI was stamped (timeout, dropped connection, or an unreadable PAC response). Timbrix confirms the outcome with the PAC automatically within a few minutes:
  • If it was stamped, the invoice appears in your invoice list and you receive the invoice.stamped webhook.
  • If it was not, you receive invoice.stamping_failed with error code PAC_STAMP_FAILED_CONFIRMED.
Do not issue the invoice again with different data. Retrying with the same idempotencyKey is safe: it returns the confirmed invoice, or 409 PAC_STAMP_PENDING while confirmation is still running.

409 Conflict — PAC_STAMP_PENDING

A previous request with the same idempotencyKey is still being confirmed with the PAC. Nothing new was stamped. Retry in a few minutes with the same key.

409 Conflict — CONFLICT

In production, the organization moved to another billing account (its ownership was transferred) while the request was running. Nothing was stamped and no CFDI was counted. Retry the request.

422 Unprocessable Entity — RFC_IN_69B_LIST

The receptor’s RFC (or yours as emisor, in production only) appears as Definitivo in the SAT’s 69-B list. Nothing is stamped. A Definitivo receptor is allowed only when the invoice is a credit note of an invoice stamped to that same RFC in the same environment, or the first substitution of a CFDI stamped to that same RFC (one with no vigente substitute and no substitute being stamped); it is then stamped with the RFC_69B_DEFINITIVO warning. See Lista 69-B del SAT.

503 Service Unavailable

Timbrix could not reach the PAC after automatic retries (DNS failure or connection refused), so the request never reached it and nothing was stamped. Reported with the PAC_UNAVAILABLE error code. Safe to retry.

Authorizations

X-API-Key
string
header
required

API Key for authentication (sk_test_... for sandbox, sk_live_... for production)

Headers

x-timbrix-client
string
required
X-Timbrix-Client
string

Canal de origen del CFDI: web, api, sdk, cli o mcp. Si no se envía o no es válido, se registra api. El SDK lo envía como sdk por defecto.

X-Organization-Id
string

Required for Supabase session auth. Ignored when authenticating with an API key (the organization resolves from the key).

Body

application/json
date
string
required
Example:

"2026-07-30T22:50:00"

use
string
required

SAT uso CFDI catalog code

Example:

"G03"

items
object[]
required
type
enum<string>

Ingreso, Egreso, or Traslado (default: I)

Available options:
I,
E,
T
Example:

"I"

Required when type is E (Egreso/nota de crédito) — UUID fiscal of the original Ingreso CFDI it credits. Must belong to this organization and be a vigente Ingreso.

Example:

"d3bfbc57-44af-4390-a064-f0afab85e5df"

CFDI relacionados. Fase actual: un solo elemento { type: "04", uuids: [<uuid del CFDI que se sustituye>] }, solo para Ingreso y Traslado. Para Egreso usa relatedInvoiceUuid.

series
string

Opcional: si no se manda se usa la serie predeterminada del tipo (I y T: A, E: NC, salvo que la organización la cambie). De 1 a 25 caracteres, sin |.

Maximum string length: 25
Example:

"A"

folioNumber
string

Opcional: si no se manda se usa el siguiente folio de la serie. Un folio que ya existe en la serie se rechaza con 409 FOLIO_ALREADY_USED. De 1 a 40 caracteres, sin |.

Maximum string length: 40
Example:

"1"

paymentForm
string

SAT forma de pago catalog code — required unless type is T (Traslado)

Example:

"01"

paymentMethod
enum<string>

Default: PUE

Available options:
PUE,
PPD
Example:

"PUE"

currency
string

Default: MXN

Example:

"MXN"

exchange
number

Default: 1

Example:

1

export
string

SAT clave de exportación catalog code — default: 01

Example:

"01"

customer
object
customerId
string

Existing customer ID — mutually exclusive with customer

idempotencyKey
string

Client-supplied key to safely retry this request without double-stamping. If an invoice was already created for this organization with the same key, that invoice is returned instead of stamping again.

Example:

"order-8421-attempt-1"

Response

id
string
required

Timbrix invoice record ID

Example:

"0f2a1c3e-1a2b-4c3d-9e8f-1234567890ab"

uuid
string
required

Folio fiscal UUID asignado por el SAT vía PAC

Example:

"d3bfbc57-44af-4390-a064-f0afab85e5df"

status
enum<string>
required

Estado fiscal del CFDI registrado en Timbrix: vigente o cancelado. Antes de TIM-243 este campo era siempre "valid". Con una cancelación pendiente (cancellationStatus: pendiente) cuyo plazo de respuesta del receptor ya venció, el SAT pudo haberlo cancelado automáticamente; confírmalo en el portal del SAT.

Available options:
vigente,
cancelado
Example:

"vigente"

cancellationStatus
enum<string> | null
required

Latest cancellation attempt's status, only while the invoice is still vigente. null if never attempted or already cancelado. A pendiente past the receptor's response deadline may already be cancelado at the SAT. en_proceso: the cancellation was sent and its outcome is being confirmed with the SAT; don't treat the CFDI as vigente yet.

Available options:
pendiente,
rechazada,
en_proceso
cancellationDesynced
boolean
required

true when a cancellation was accepted but the invoice status update failed to apply — indicates the invoice may already be cancelled at the SAT even though it still shows vigente here; needs manual reconciliation.

Example:

false

type
enum<string>
required

I = Ingreso, E = Egreso, T = Traslado, P = Pago

Available options:
I,
E,
T,
P
Example:

"I"

series
string
required
Example:

"A"

folioNumber
string
required
Example:

"1"

total
number
required
Example:

116

date
string
required
Example:

"2026-07-30T22:50:00"

xml
string
required

XML del CFDI timbrado (UTF-8)

createdAt
string
required
Example:

"2026-07-30T22:50:03.412Z"

environment
enum<string>
required

Ambiente en el que se timbró el CFDI. Los CFDI de sandbox no tienen validez fiscal ante el SAT.

Available options:
sandbox,
production
Example:

"production"

channel
enum<string> | null
required

Canal de origen declarado por el cliente con el header X-Timbrix-Client (api si no se envió). null para CFDI anteriores a este campo.

Available options:
web,
api,
sdk,
cli,
mcp
Example:

"mcp"

rfcReceptor
string
required

RFC del receptor del CFDI

Example:

"GODE561231GR8"

Relaciones de este CFDI (CfdisRelacionados). Con tipo 04 apunta al CFDI que sustituye.

substitutedBy
object[]
required

CFDI cuya relación 04 apunta a este comprobante (sustitutos emitidos).

paymentMethod
enum<string> | null
required

Método de pago; null en Traslado, Pago y facturas anteriores a este campo.

Available options:
PUE,
PPD
Example:

"PPD"

balance
object | null
required

Saldo pendiente de una factura PPD (total menos complementos de pago vigentes o en timbrado); null en las demás.

Example:

160

payment
object

Solo en complementos de pago (type P): el pago y las facturas que liquida.

warnings
object[]

Solo si hay advertencias de la lista 69-B del SAT (receptor Presunto). Un replay por idempotencyKey no las recalcula.