Factura global de tickets
Los tickets de Auto CFDI que tus clientes no facturan van a una factura global que emites con Crear factura, la misma petición de cualquier factura global. Lo único propio es el campo ticketId en cada concepto: con él, Fiscalapi verifica que el concepto refleje el ticket y lo marca como facturado en la global.
Una factura global sin ticketId en sus conceptos no marca ningún ticket: siguen en pending y tus clientes los pueden autofacturar, lo que factura dos veces la misma venta. Incluye el ticketId de cada ticket que ampara la global.
Armar la factura global
Arma una global por cada combinación de persona emisora, moneda, tipo de cambio y lugar de expedición: el CFDI lleva un solo valor de cada uno, y el código postal del receptor público en general es el lugar de expedición. Una factura admite como máximo 1,500 conceptos; si tus tickets necesitan más, repártelos en varias globales sin partir un ticket entre dos.
1. Consulta los tickets pendientes
Consulta los tickets en pending del emisor en el periodo con Listar tickets y details=true, que trae los conceptos con sus impuestos.
La lista es paginada: repite la consulta con el siguiente pageNumber mientras hasNextPage sea true.
GET /api/v4/tickets?status=pending&personId=<id>&from=2026-10-01&to=2026-10-31&details=true&pageNumber=1&pageSize=50
2. Arma un concepto por ticket y grupo de impuestos
Cada ticket se convierte en un concepto por cada grupo de sus conceptos con el mismo objeto de impuesto y los mismos impuestos. Fiscalapi calcula todos los impuestos de un concepto sobre la misma base, así que un ticket con productos al 16 % y al 0 % de IVA lleva dos conceptos, ambos con el mismo ticketId.
| Campo del concepto | Valor |
|---|---|
ticketId | ID del ticket. |
itemCode | 01010101 |
itemSku | Folio del ticket. |
quantity | 1 |
unitOfMeasurementCode | ACT |
description | Venta |
unitPrice | Suma de la cantidad por el valor unitario de los conceptos del grupo. |
discount | Suma de los descuentos de los conceptos del grupo. |
taxObjectCode e itemTaxes | Los del grupo, sin cambios. |
Fiscalapi compara, por cada ticket, la suma de sus conceptos en la global contra el ticket guardado: importes, descuentos e impuestos por impuesto, tipo de factor y tasa. Si no coinciden, responde 400 en el ticketId del primer concepto del ticket, sin cobrar.
3. Completa el comprobante
La global de tickets lleva el emisor por referencia (issuer.id), la persona emisora de los tickets. El receptor es el público en general y la forma de pago es la del ticket de mayor monto, según la guía de llenado de la factura global del SAT.
| Campo | Valor |
|---|---|
typeCode | I |
paymentMethodCode | PUE |
exportCode | 01 |
paymentFormCode | La forma de pago del ticket de mayor monto. |
currencyCode y exchangeRate | Los de los tickets. Con MXN, el tipo de cambio es 1. |
expeditionZipCode | El de los tickets. |
issuer.id | ID de la persona emisora de los tickets. |
recipient | RFC XAXX010101000, PUBLICO EN GENERAL, régimen 616, uso S01 y el código postal de expedición. |
globalInformation | Periodicidad, mes y año del periodo. Ver Información global. |
Crear factura global de tickets
Este endpoint timbra la factura global con la petición estándar de Crear factura. Con ticketId, Fiscalapi reserva los tickets antes de cobrar el timbre: todos o ninguno. Al guardar el CFDI, los tickets pasan a globalized con el ID de la factura en globalInvoiceId. Si el PAC rechaza el CFDI, los tickets vuelven a su estado anterior y el timbre se reembolsa.
El ejemplo ampara el ticket F-1002, una venta de 166.00 con un refresco de 100.00 al 16 % de IVA y agua natural de 50.00 al 0 %. Sus dos grupos de impuestos van en dos conceptos con el mismo ticketId, y cada concepto suma lo que el ticket guardó en su grupo.
Reglas de ticketId
El campo items[].ticketId es opcional. Sin él, la factura se emite igual que siempre.
| Regla | Qué pasa si no se cumple |
|---|---|
Solo va en una factura global de ingreso, con typeCode I y globalInformation. | 400 en el ticketId del concepto. |
| La global lleva el emisor por referencia. | 400 en Issuer.Id. |
| El ticket es de tu cuenta, de la misma persona emisora y en la misma moneda que la global. | 400 en el ticketId del concepto. |
| Los conceptos del ticket reflejan el ticket guardado. | 400 en el ticketId del primer concepto del ticket. |
El ticket está en pending, o en globalized en la global que sustituyes. | 409 con data.unavailableTickets, sin cobrar ni tomar tickets. |
Reintentos
Un 400 o un 409 no cobran el timbre ni dejan tickets tomados. Ante un 409, data.unavailableTickets trae el id y el estado de cada ticket que ya no estaba disponible: vuelve a consultar los tickets pendientes y arma la global otra vez. No reintentes la misma petición sin cambios.
Si el PAC no responde o responde un error de su servidor sin código de rechazo, el resultado es incierto porque el CFDI pudo timbrarse. Fiscalapi responde el error inesperado y conserva el timbre, el folio y los tickets tomados para revisarlos. No emitas otra global con esos tickets: consulta tus facturas y los tickets, y si siguen en globalizing, escribe a soporte con el traceIdentifier de la respuesta.
Errores
Las fallas llegan como falla de validación, con el campo en PascalCase; por ejemplo, Items[0].TicketId. En los mensajes, i es la posición del concepto, desde 0.
| Estado | Cuándo ocurre | Mensaje |
|---|---|---|
| 400 | Un concepto trae ticketId y la factura no es una global de ingreso. | 'items[i].ticketId' solo aplica en una factura global de ingreso: 'typeCode' I con 'globalInformation'. Quita el ticketId o emite la factura global. |
| 400 | El ticketId va vacío o pasa de 36 caracteres. | 'items[i].ticketId' debe tener de 1 a 36 caracteres: el id del ticket. |
| 400 | La global con tickets lleva el emisor por valores. Llega en Issuer.Id. | La factura global con tickets requiere el emisor por referencia ('issuer.id'): la persona emisora de los tickets. |
| 400 | El ticket no existe o es de otra cuenta. | 'items[i].ticketId' no es un ticket de tu organización. |
| 400 | El ticket es de otra persona emisora. | 'items[i].ticketId' es un ticket de otra persona emisora: la factura global lleva solo tickets de su emisor, indicado por referencia ('issuer.id'). |
| 400 | El ticket está en otra moneda. | 'items[i].ticketId' es un ticket en otra moneda: la factura global lleva solo tickets de su moneda ('currencyCode'). |
| 400 | Los conceptos del ticket no reflejan el ticket guardado. | Los conceptos con el ticket de 'items[i].ticketId' (<conceptos>) no reflejan ese ticket: la suma de sus importes (quantity × unitPrice), la de sus descuentos y la de sus impuestos por impuesto, tipo de factor y tasa deben ser las de los conceptos del ticket. Un ticket con impuestos distintos va en un concepto por cada grupo de impuestos, todos con el mismo ticketId. |
| 409 | Un ticket ya no está disponible. Los tickets y su estado van en data.unavailableTickets. | Uno o más tickets ya no están disponibles para la factura global (solo entra un ticket en estado pending): 'data.unavailableTickets' dice cuáles y su estado. No se cobró el timbre ni quedó tomado ningún ticket. |
En el mensaje de conceptos que no reflejan el ticket, <conceptos> es la lista de los conceptos con ese ticket; por ejemplo, items[0], items[1]. Los demás errores de la factura son los de Crear factura global.
Request
curl --location 'https://test.fiscalapi.com/api/v4/invoices' \
--header 'X-TENANT-KEY: <tenant_key>' \
--header 'X-TIME-ZONE: America/Mexico_City' \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: <api_key>' \
--data-raw '{
"versionCode": "4.0",
"series": "G",
"date": "2026-11-01T10:00:00",
"paymentFormCode": "01",
"paymentMethodCode": "PUE",
"currencyCode": "MXN",
"exchangeRate": 1,
"typeCode": "I",
"expeditionZipCode": "42501",
"exportCode": "01",
"globalInformation": {
"periodicityCode": "04",
"monthCode": "10",
"year": 2026
},
"issuer": { "id": "<person_id>" },
"recipient": {
"tin": "XAXX010101000",
"legalName": "PUBLICO EN GENERAL",
"zipCode": "42501",
"taxRegimeCode": "616",
"cfdiUseCode": "S01"
},
"items": [
{
"ticketId": "<ticket_id>",
"itemCode": "01010101",
"itemSku": "F-1002",
"quantity": 1,
"unitOfMeasurementCode": "ACT",
"description": "Venta",
"unitPrice": 100.000000,
"discount": 0,
"taxObjectCode": "02",
"itemTaxes": [
{ "taxCode": "002", "taxTypeCode": "Tasa", "taxRate": "0.160000", "taxFlagCode": "T" }
]
},
{
"ticketId": "<ticket_id>",
"itemCode": "01010101",
"itemSku": "F-1002",
"quantity": 1,
"unitOfMeasurementCode": "ACT",
"description": "Venta",
"unitPrice": 50.000000,
"discount": 0,
"taxObjectCode": "02",
"itemTaxes": [
{ "taxCode": "002", "taxTypeCode": "Tasa", "taxRate": "0.000000", "taxFlagCode": "T" }
]
}
]
}'
Response 409
{
"data": {
"unavailableTickets": [
{ "ticketId": "9f8e7d6c-1b2a-4c3d-8e9f-0a1b2c3d4e5f", "status": "invoiced" }
]
},
"succeeded": false,
"message": "Sorry, it's not me, it's you.",
"details": "Uno o más tickets ya no están disponibles para la factura global (solo entra un ticket en estado pending): 'data.unavailableTickets' dice cuáles y su estado. No se cobró el timbre ni quedó tomado ningún ticket.",
"httpStatusCode": 409
}
Sustituir una factura global de tickets
Para excluir tickets de una global ya timbrada, por ejemplo porque un cliente pidió su factura después, sustitúyela con los endpoints estándar: emite una nueva global con los tickets que se quedan y cancela la original. La guía de llenado de la factura global del SAT describe este caso: la nueva global sin la operación que se factura de forma nominativa, relacionada con la original por sustitución.
Global original (vigente) Tickets A, B y C en globalized
|
| 1. POST /api/v4/invoices
| relatedInvoices: tipo 04 con el UUID de la original
| items[].ticketId: solo A y B
v
Global sustituta (vigente) A y B pasan a la sustituta (globalized)
C sigue en globalized en la original
|
| 2. DELETE /api/v4/invoices
| id de la original, motivo 01 y el UUID de la sustituta
v
Original en Cancelando --> el SAT confirma la cancelación
|
v
Original cancelada C vuelve a pending
Cada paso es una petición que puedes reintentar, y el estado lo dan los tickets y las facturas:
| Paso | Petición | Qué cambia |
|---|---|---|
| 1 | Crear factura con relatedInvoices de tipo 04 y el UUID de la original, y el ticketId de los tickets que se quedan. | Al guardar la sustituta, sus tickets pasan de la original a ella. |
| 2 | Cancelar factura de la original con motivo 01 y la sustituta en replacementUuid. | La original pasa a Cancelando hasta que el SAT confirma. |
| 3 | Ninguna: Fiscalapi consulta al SAT. | Confirmada la cancelación, los tickets excluidos vuelven a pending. |
Para armar la sustituta, consulta los tickets de la original con Listar tickets y globalInvoiceId con el ID de la original. Copia de la original la serie, la persona emisora, la información global, la moneda, el tipo de cambio y el lugar de expedición, y vuelve a calcular la forma de pago con los tickets que se quedan.
Mientras la original siga vigente, sus tickets excluidos siguen en globalized y el portal no los autofactura. Cuando se confirma la cancelación de una global, cada uno de sus tickets vuelve a la global vigente más reciente que lo amparaba antes, o a pending si no hay ninguna. Por ejemplo, si cancelas la sustituta mientras la original sigue vigente, sus tickets vuelven a la original. Así, una venta nunca queda en pending mientras un CFDI vigente la ampara.
Si la original solo amparaba tickets que excluyes, no hay sustituta: cancélala con el motivo que corresponda a tu caso, sin relación. Consulta los motivos en Cancelar factura.