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.

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". *** 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 organization_legal, matching how Facturapi resolves the issuer from the account profile instead of requiring it on every request.
  • When an item uses productId, its description, unitPrice, productKey, unitKey, and taxes are all resolved from the stored product — you only need to send quantity, amount, and productId.
  • 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).
See docs/PAC_CFDI_CONTRACT.md in the repository for the internal payload contract this endpoint translates into before calling the PAC.

Idempotency

Stamping is not safe to blindly retry — if your request times out, you can’t tell whether the CFDI was actually stamped at the SAT before the timeout. Pass a unique idempotencyKey (e.g. your own order ID) and retry with the same key: if an invoice was already created for your organization with that key, the existing invoice is returned instead of stamping a second one.
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
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

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.

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, or productId does not exist — in either case, belonging to a different organization than the authenticated one counts as not found.

503 Service Unavailable

The PAC did not respond after automatic retries (network failure, timeout, or a transient PAC error). Reported with the PAC_UNAVAILABLE error code. Safe to retry.

Authorizations

X-API-Key
string
header
required

API Key for authentication (format: sk_...)

Headers

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
series
string
required
Example:

"A"

folioNumber
string
required
Example:

"1"

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"

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
Available options:
valid
Example:

"valid"

type
enum<string>
required

I = Ingreso, E = Egreso, T = Traslado

Available options:
I,
E,
T
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"