Skip to main content
Some operations in this guide have Discovery handlers but no public gateway route yet: GET /invoices/payment/{id}, POST /invoices/{id}/support-documents. Check the availability notice on each API reference page before using them.
Start with shared fields, then the Mexico or Colombia guide. The CFDI, RFC, SAT catalog, and payment-complement examples below describe Mexico; they are not universal country requirements.
Create, manage, and cancel CFDI 4.0 compliant invoices with full SAT integration. The Invoices API handles the complete invoice lifecycle including creation, stamping, cancellation, and payment complements.

Overview

The Invoices API provides comprehensive invoicing capabilities with Mexican tax compliance. Create PUE (single payment) or PPD (partial payments) invoices, manage payment complements, and handle cancellations with SAT.

Key Features

  • CFDI 4.0 Compliance - Full SAT specification support
  • Automatic Stamping - Real-time SAT integration
  • Payment Complements - PPD invoice management
  • Cancellation Support - SAT-compliant cancellation process
  • Global Invoices - Monthly global invoice generation
  • File Generation - Automatic PDF and XML creation
  • Draft Invoices - Prepare, preview, and approve invoices before stamping
  • Batches - Up to 1,000 income invoices per request, stamped in the background (Invoice Batches)
  • Income-invoice retry protection - Use the documented idempotency_key flow on income creation; other operations have their own retry rules
  • Automation Options - Flexible workflow automation

Endpoints

List CFDI Errors

Retrieve a comprehensive catalog of CFDI error codes with descriptions, explanations, and solutions. This endpoint is essential for implementing proper error handling and providing meaningful feedback when invoice operations fail. Query Parameters:
  • code (string) - Filter by exact error code (e.g., CFDI140223)
  • q (string) - Search across code, description, explanation, and solution
  • type (string) - Filter by error type: invoice, receiver, sender, unknown
  • limit (integer, 1-100) - Number of results per page (default: 50)
  • page (integer) - Page number for pagination (default: 1)
Example Request:
Example Response:
For complete documentation on the CFDI errors endpoint, see the CFDI Errors Reference.

List Income Invoices

Retrieve a paginated list of income invoices with filtering capabilities. Query Parameters: Metadata Filtering: You can filter invoices by any metadata key using either dot notation or underscore notation. Both formats are equivalent:
Metadata filtering uses Typesense search for efficient querying without requiring Firestore indexes. When using metadata filters, pagination is controlled via the page parameter instead of the next cursor. Example Requests:
Example Response:

Create Income Invoice

Create a new income invoice with optional automation. Automation Types:
  • payment - Create invoice with payment automation
  • none - No automation, create invoice only
Request Body:
Example Request with Client Search:
To issue many invoices at once, send the same bodies to POST /invoices/income/batch, up to 1,000 per request.

Idempotency and safe retries

Send your own identifier for the invoice, for example your order id, as idempotency_key. gigstack claims the key before it charges a credit or stamps, so sending the same request again can never issue a second CFDI or charge twice. What a request with a used key gets: The duplicate answer looks like this:
Keys are scoped to your team and to the credential’s mode. You can look an invoice up by its key with GET /invoices/income?idempotency_key=…. Without an idempotency_key none of this protection applies (see 503).

Get Income Invoice

Retrieve a specific income invoice by ID. Example Request:

Create Egress Invoice

Create a new egress invoice (credit note / nota de crédito). Request Body:
payment_method is optional and defaults to PUE, which is how every egress invoice was stamped before the field was accepted. Set it to PPD when the credit note applies to a partial/deferred-payment invoice. With PPD, SAT requires payment_form to be 99 (“Por definir”); any other value you send is overridden to 99 rather than rejected. A PPD egress invoice is not a valid target for a payment complement — ppd_invoice_id on POST /payments only accepts income invoices.

List Egress Invoices

Retrieve a paginated list of egress invoices (credit notes) with filtering capabilities. Query Parameters: Metadata Filtering: You can filter egress invoices by any metadata key using either dot notation or underscore notation. Both formats are equivalent:
Metadata filtering uses Typesense search for efficient querying without requiring Firestore indexes. When using metadata filters, pagination is controlled via the page parameter instead of the next cursor. Example Requests:

Get Invoice Files

Retrieve XML and PDF files for an invoice. Query Parameters:
  • file_type (string) - Type of file: “pdf”, “xml” (optional, returns both if not specified)
  • team (string) - gigstack Connect: Target team ID
Example Request:
Example Response:

Cancel Invoice

For a Mexican issuer, request cancellation of the intended issued CFDI. This operation does not refund the payment. Deleting an unissued draft is a separate operation. Colombia uses different provider behavior; start with its country guide. Use the motive that represents the actual transaction. For motive 01, first issue and retain the correct replacement and its relationship to the original; then cancel the original using the replacement’s UUID. The motive catalog explains the codes. Use the environment and key from the quickstart. Set INVOICE_UUID to the reviewed invoice in that team and mode. The following is a source-checked request example, not a claim that cancellation was live-tested:
For motive 01, the body is {"motive":"01","substitution_uuid":"REPLACEMENT_UUID"}. The endpoint returns the provider result directly, rather than a standard data envelope. HTTP 200 acknowledges a result, not necessarily final cancellation. Afterward, retrieve GET /invoices/{id} again. The current Mexican flow may store cancellation_status: "pending" and keep status: "valid" if its SAT status check does not agree with the provider’s answer, even when the immediate response says accepted. Report cancellation as final only after reconciling the stored status and the provider/SAT outcome. Preserve the cancellation receipt when available. There is no documented cancellation idempotency key. Do not create a replacement or repeat cancellation merely because the first response was lost. See error handling for provider errors.

Create Payment Complement (Complemento de Pago)

Stamps a CFDI type P (Pagos 2.0) that records one or more payments against PPD invoices. Each entry in complements[].data is one payment, and each payment lists the PPD invoices it pays in related_documents. The SAT fixes some values, so you do not send them: the comprobante currency (XXX), the receptor’s UsoCFDI (CP01) and the line concept. The series defaults to the team’s payments series. Request Body: Each payment (complements[].data[]): Each related document: uuid (the PPD invoice’s folio fiscal), amount (paid now), installment (1 for the first payment against that invoice), last_balance (balance before this payment) and currency; optionally exchange, series, folio_number and taxes. Example Request:
Response: 200 with { "message": "Payment complement created", "data": { …invoice } }. This endpoint returns a raw body, not the standardized envelope. A body that fails validation returns 400 with { "message": "Invalid body", "errors": [ … ] }. List and read complements with GET /invoices/payment and GET /invoices/payment/{id}, which work like the income-invoice list and get.

Create Transfer Invoice (Traslado with Carta Porte)

Stamps a CFDI type T (traslado) with the Carta Porte 3.1 complement, for moving your own goods by road (autotransporte). It is the document that travels with the truck. The SAT fixes the comprobante, so you do not send it: currency XXX, Total 0, UsoCFDI S01, no payment method. There are no items; the CFDI concepts are built from carta_porte.Mercancias.Mercancia. The series defaults to T. Only teams that stamp through gigstack’s own PAC (CSD uploaded in gigstack) can use this endpoint. Request Body: carta_porte (numbers can be numbers or numeric strings): Example:
A 400 lists the fields that failed, including the Carta Porte rules (for example carta_porte.Ubicaciones[1].DistanciaRecorrida is required on a Destino). PAC rejections come back with the PAC’s message.

Search Invoices

Full-text, typo-tolerant search across client name, email, invoice UUID, description and metadata.
The response carries data plus found (total matches), page and per_page; there is no cursor. Search must be enabled for your team — otherwise the call returns 400 with error.code: missing_typesense_key. A missing q returns 400 missing_query.

Support Documents

Attach the evidence the SAT may request to validate an invoice — contracts, proof of delivery, proof of payment. The same two endpoints exist for clients (/clients/{id}/support-documents) and payments (/payments/{id}/support-documents). Upload as multipart/form-data:
Answers 201 with the stored document in data (id, document_type, file_url, compliance_status: "pending_review", …) in the standardized envelope. file_url is a Firebase download-token URL — it does not expire, but treat it as opaque and re-read the document instead of building the link yourself. GET lists the invoice’s documents, newest first. An unknown invoice returns 404. To review, link or analyze documents independently of one invoice, use the Documents API.

End-of-Month Global Invoicing

Manually triggers the end-of-month process that groups your pending receipts into global invoices. Three preconditions:
  • A live API key. Test keys get 403 (error.code: operation_not_allowed).
  • It must be the last calendar day of the month in America/Mexico_City. Any other day returns 400 (operation_not_allowed), with today’s date and the next eligible date in error.details.
  • It must be before 23:00 in America/Mexico_City. From 23:00 the automatic end-of-month run takes over, and manual calls return 400 (operation_not_allowed).
The body is ignored. The call returns as soon as the run is triggered and does not wait for it to finish:
If the downstream process rejects the trigger, the call returns 502 (external_service_error) and nothing is invoiced.

Draft Invoices (Pre-Facturas)

Draft invoices allow you to prepare and review an invoice before stamping it with SAT. This is useful for approval workflows, client review, or building invoices incrementally over time. A draft follows this lifecycle:
  1. Create a draft with partial or complete data.
  2. Update the draft as needed (add items, set client, change payment method).
  3. Preview the draft to generate a PDF with a “Sin Validez Fiscal” watermark.
  4. Stamp the draft to finalize it into a real CFDI invoice.
Drafts are stored in the invoices collection with a draft: true flag and do not consume credits until stamped.

Create Draft

Create a new draft invoice. Only invoice_type is required at creation; all other fields can be added later via update. Request Body:
Minimal Request (just the type):
Example Request:
Response (201):

List Drafts

Retrieve a paginated list of draft invoices. Query Parameters: Example Request:

Get Draft

Retrieve a specific draft by ID. If a preview PDF has been generated, it is included in the response. Example Request:

Update Draft

Send the complete intended request body, including the client, invoice type, items, fiscal fields, delivery settings and metadata you intend to keep. Although the validator accepts omitted fields, the mapper assigns defaults: a notes-only update cleared items in the staging verification. Do not treat this operation as a partial PATCH. Keep your original request JSON in your application. Using draft-body.json from the invoice recipe, create a complete revised request:
Compare the returned items, recipient, type, totals and mode with your intended request. Preview the revised draft again before stamping. Do not send an entire GET response back as a request: output fields are not all accepted input fields.

Delete Draft

Permanently delete a draft and its associated preview files. Example Request:

Stamp Draft (Finalize)

Check the stored draft mode before stamping. A test key alone does not convert a live draft to test mode. Use a draft created with your test key for the tutorial.
Finalize a draft into a real CFDI invoice. This stamps the invoice with SAT, assigns a folio, and removes the draft document. The draft must have all required fields before stamping:
  • client with valid fiscal data
  • At least one item
  • use (CFDI use code)
  • payment_form
  • payment_method
  • currency
Optional Request Body:
  • send_email (boolean) - Set to false to suppress email delivery. Default: true.
  • return_files (boolean) - Set to true to include base64-encoded XML and PDF in the response.
Example Request:
Response (200):
Error: Incomplete draft (400):
Error: Credit limit reached (429):

Preview Draft

Generate a preview PDF for the draft. The PDF includes a “Sin Validez Fiscal” watermark and uses a placeholder UUID (PREFACTURA-0000-0000-0000-SINVALIDEZ). The preview is saved to the draft’s files subcollection. The draft must have at least a client and one item. Example Request:
Response (200):

Draft Workflow Example

Follow Draft, preview, and issue for one complete sequence with saved request files, captured IDs, a decoded preview, and retrieval of the issued invoice. For an approval workflow:
  1. Save the intended request JSON and create the draft.
  2. If anything changes, send the full revised body as described in Update Draft.
  3. Fetch and compare the draft, then generate and review a fresh preview.
  4. Verify the stored draft’s livemode and issue only the reviewed draft.
  5. Save data.uuid from the stamping response and retrieve the issued invoice with that UUID. The former draft ID returned 404 after stamping in the verified flow.
The sequence’s concrete expected responses are recorded in verification. An issued invoice is a fiscal document; saving or previewing a draft is not issuance.

Invoice Structure

Invoice Types

  • I - Ingreso (Income)
  • E - Egreso (Expense / Credit note)
  • P - Pago (Payment complement)
  • T - Traslado (Transfer of goods, with Carta Porte)
  • N - Nomina (Payroll)

Payment Methods

  • PUE - Pago en Una sola Exhibicion (Single payment)
  • PPD - Pago en Parcialidades o Diferido (Partial or deferred payment)

Payment Forms (Formas de Pago)

Complex Invoice Examples

Invoice with Multiple Items and Taxes

Invoice with Withholding Taxes

PPD Invoice (Partial Payments)

Global Invoice

The receiver of a global invoice is the general public (XAXX010101000, regime 616, use S01, your own postal code). See Global Invoices for the periodicity and month codes.
Relationship Types:
  • 01 - Nota de credito de los documentos relacionados
  • 02 - Nota de debito de los documentos relacionados
  • 03 - Devolucion de mercancia sobre facturas o traslados previos
  • 04 - Sustitucion de los CFDI previos
  • 07 - CFDI por aplicacion de anticipo

Common Scenarios

1. Simple Sale Invoice

2. Service Invoice with Email

3. USD Invoice with Exchange Rate

Best Practices

  1. Use idempotency keys - Prevent duplicate invoices by including unique identifiers.
  2. Validate clients first - Ensure the client’s fiscal data (RFC, tax system, address) is correct before creating an invoice.
  3. Configure email settings - Set up BCC addresses for your accounting department.
  4. Use correct payment methods - Use PUE for immediate single payments and PPD for partial or deferred payments.
  5. Include all required taxes - Apply IVA, ISR, and IEPS as applicable to each line item.
  6. Set proper CFDI use - Match the CFDI use code to the client’s tax requirements.
  7. Keep series organized - Use different series for different invoice types or business units.
  8. Use drafts for review - For high-value invoices, use the draft workflow to generate a preview before stamping.

Error Handling

When working with invoices, you may encounter various CFDI-specific error codes. Use the CFDI Errors endpoint to look up detailed explanations and solutions for any error codes you receive.

At a glance

Two error shapes

CFDI failures use a dedicated shape:
  • code — the SAT/PAC error code, or one of gigstack’s own (SAT_NOT_CONNECTED, PAC_UNAVAILABLE, PAC_OUTCOME_UNKNOWN, STAMP_NEEDS_REVIEW, CSD_VALIDATION_ERROR, STAMPING_ERROR, …).
  • providerMessage — present only on 400 stamping rejections. It is the PAC’s verbatim text; log it, it names the offending field and value.
  • retryable — true for 503 PAC_UNAVAILABLE, and for 503 PAC_OUTCOME_UNKNOWN only when the request carried an idempotency_key. Treat it as authoritative: nothing else is worth retrying unchanged.
  • duplicate and uuid — only on the 400 for an idempotency_key whose invoice already exists (see Idempotency and safe retries).
  • The trailing [pcs_…] is the process-log id. Include it in any support request.
Shape gotcha: on the create endpoints (POST /invoices/income, /egress, /payment, /draft/{id}/stamp) this object arrives nested under message — {"message": {"error": …, "code": …}}. On DELETE /invoices/{id} (cancel) it arrives at the top level. Parse defensively: body.message?.code ?? body.code.
Everything else uses the standard envelope:

412 — SAT not connected

The team has no usable invoicing provider — either the SAT connection was never completed, or the CSD failed validation (code: "CSD_VALIDATION_ERROR"). This is a precondition on the team, not a problem with your payload; the request never reached the PAC. Do: stop retrying. Upload/refresh the CSD via POST /v2/teams/{id}/sat-connection, or send the team owner to the integrations page. Under Connect, check that ?team= points at a team that has actually finished onboarding — this is the usual cause when one connected team works and another doesn’t.

503 — PAC unavailable or outcome unknown

A 503 from a create endpoint carries one of two codes. They mean different things, so read code. PAC_UNAVAILABLE
The stamping provider couldn’t be reached, or refused the request before stamping (for example HTTP 429, maintenance). The document never reached the SAT, so nothing in your data is wrong and no CFDI exists. Do: retry with exponential backoff (a few minutes is usually enough). Keep the same idempotency_key. Don’t show a validation error to your end user; nothing they entered is at fault. PAC_OUTCOME_UNKNOWN
The request reached the PAC and its answer was lost (a timeout, a dropped connection, an HTTP 5xx, an unreadable response). The CFDI may exist. Its folio is never reused for another invoice. Timeouts and 5xx answers from the PAC used to be reported as PAC_UNAVAILABLE; they are now this code.
  • With an idempotency_key (retryable: true): retry with the same key. gigstack resends the exact same XML with the same folio, so the PAC either stamps it now or returns the stamp it already made. It can’t stamp twice, and the retry isn’t charged again. If the PAC can’t tell, the retry answers 409 STAMP_NEEDS_REVIEW.
  • Without one (retryable: false): a new request would take a new folio and could issue a second CFDI. Check whether the invoice exists (in GET /invoices/income, or in the SAT) before sending it again. Sending an idempotency_key on every create avoids this.

409 — Stamp needs review

An earlier attempt with this idempotency_key reached the PAC, and gigstack could not prove whether it was stamped: the PAC said it already stamped the document but returned no UUID, the SAT’s 72-hour window for the document had passed, or the invoice was stamped but couldn’t be saved. Every later request with the key gets this answer, so the invoice can’t be issued twice. Do: contact support with the idempotency_key and the [pcs_…] id. Don’t send the invoice again under a new key.

429 — Team credit limit reached

Not a rate limit. The team has a configured creditLimit on issued documents and has consumed it. The counter increments before stamping, so this is checked and rejected up front — no folio is consumed. Note the shape: credit_limit and used_credits are returned at the top level so you can display the exact quota. POST /v2/receipts reports the same condition in the standard envelope with error.code = "team_credit_limit_reached". Do: stop and raise the limit (credit_limit on PUT /v2/teams/{id}) or contact gigstack. Backing off does not help — the counter only moves when the limit is raised.

422 — Imported invoice cannot be cancelled

Returned by DELETE /invoices/{id} when the invoice arrived via import (source: "import") and carries no gigstack stamp. gigstack holds no credentials for the PAC that issued it, so it cannot cancel it on your behalf. Do: cancel it in the system that issued it. There is no request you can change to make this succeed. POST /invoices/sat/{uuid}/retry-xml also uses 422 for a failed XML fetch from SAT, with data.resource_status: "error" — that one is worth retrying later.

502 — Upstream service error

An internal downstream service failed — most commonly the CFDI-to-PDF renderer behind POST /invoices/sat/{uuid}/pdf. Your invoice data is fine and, for PDF generation, the CFDI itself is untouched. Do: retry with backoff. If it persists, the XML is still available through GET /invoices/{id}/files.

409 — Concurrent resource creation

Two requests tried to auto-create the same client or service (via client.search.auto_create / item search.auto_create) at the same time. One won; yours was told to wait rather than create a duplicate. Do: retry once after a short delay — the resource will exist by then. POST /v2/clients also returns 409 for a different reason: a search that matched more than one client. That one is not retryable; narrow the search or pass the client id.

409 — Idempotency key in progress

Another POST /invoices/income with the same idempotency_key is being processed right now, for example a retry sent while the first request was still waiting for the PAC. Nothing was charged or stamped by this request. Do: wait a few seconds and retry with the same key. Once the first request finishes you get its outcome, for example the 400 duplicate with the invoice’s uuid. If the first request died mid-way, the key stays held for up to 10 minutes.

Missing Required Fields (400)

Cancellation Error

Note the flat shape here — DELETE /invoices/{id} does not nest the CFDI error under message:
The provider refused the cancellation. Read providerMessage to identify the actual cause, such as a dependent document or a recipient-acceptance requirement. Do not interpret an error as a universal cancellation deadline. Resolve the stated cause and reconcile the invoice status before repeating the request.
For additional help with invoice management, refer to the support documentation or contact support@gigstack.io.