> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gigstack.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Invoices API Guide

> Integration guide for Invoices

<Warning>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.</Warning>

<Note>Start with [shared fields](/concepts/shared-fields), then the [Mexico](/countries/mexico) or [Colombia](/countries/colombia) guide. The CFDI, RFC, SAT catalog, and payment-complement examples below describe Mexico; they are not universal country requirements.</Note>

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](/guides/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

```http theme={null}
GET /invoices/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:**

```bash theme={null}
curl -X GET "https://api.gigstack.io/v2/invoices/errors?type=receiver&limit=20" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

**Example Response:**

```json theme={null}
{
    "success": true,
    "data": [
        {
            "code": "CFDI140223",
            "description": "El campo Rfc del receptor no es valido",
            "explanation": "The RFC (tax ID) provided for the receiver does not meet the validation requirements or format specified by SAT",
            "solution": "Verify that the receiver's RFC is correct, properly formatted (13 characters for individuals, 12 for legal entities), and matches SAT's registered information",
            "type": "receiver"
        }
    ],
    "total": 1,
    "page": 1,
    "limit": 20,
    "message": "CFDI errors retrieved successfully",
    "timestamp": "2025-12-19T10:30:00.000Z"
}
```

For complete documentation on the CFDI errors endpoint, see the [CFDI Errors Reference](/guides/catalogs/cfdi_errors).

### List Income Invoices

```http theme={null}
GET /invoices/income
```

Retrieve a paginated list of income invoices with filtering capabilities.

**Query Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `limit` | integer (1-100) | Number of results per page (default: 10) |
| `next` | string | Pagination cursor for next page |
| `team` | string | gigstack Connect: Target team ID |
| `order_by` | string | Field to sort by (e.g., `created_at`) |
| `sort` | string | Sort direction (`asc`, `desc`) |
| `created_gte` | integer | Filter by creation date (greater than or equal to timestamp) |
| `created_lte` | integer | Filter by creation date (less than or equal to timestamp) |
| `client_id` | string | Filter by the gigstack client ID (e.g., `client_id=client_xxx`) |
| `tax_id` | string | Filter by the client's tax ID / RFC (e.g., `tax_id=PEGJ800101ABC`) |
| `metadata.{key}` | string | Filter by metadata field using dot notation (e.g., `metadata.order_id=ORD-123`) |
| `metadata_{key}` | string | Filter by metadata field using underscore notation (e.g., `metadata_order_id=ORD-123`) |
| `page` | integer | Page number for pagination when using metadata filters (default: 1) |

**Metadata Filtering:**

You can filter invoices by any metadata key using either dot notation or underscore notation. Both formats are equivalent:

```bash theme={null}
# Dot notation
GET /invoices/income?metadata.order_id=ORD-123

# Underscore notation (alternative)
GET /invoices/income?metadata_order_id=ORD-123
```

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:**

```bash theme={null}
# Basic listing
curl -X GET "https://api.gigstack.io/v2/invoices/income?limit=20" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Filter by creation date range
curl -X GET "https://api.gigstack.io/v2/invoices/income?created_gte=1700000000000&created_lte=1710000000000" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Filter by metadata (dot notation)
curl -X GET "https://api.gigstack.io/v2/invoices/income?metadata.order_id=ORD-123" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Filter by metadata (underscore notation)
curl -X GET "https://api.gigstack.io/v2/invoices/income?metadata_project_id=PROJ-456" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Multiple metadata filters with pagination
curl -X GET "https://api.gigstack.io/v2/invoices/income?metadata.department=sales&metadata.region=north&page=2&limit=20" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

**Example Response:**

```json theme={null}
{
    "message": "Invoices retrieved successfully",
    "data": [
        {
            "uuid": "invoice_1234567890",
            "client": {
                "id": "client_1234567890",
                "name": "Juan Perez Garcia",
                "tax_id": "PEGJ800101ABC"
            },
            "folio_number": 123,
            "series": "A",
            "invoice_type": "I",
            "total": 1160.0,
            "subtotal": 1000.0,
            "taxes": 160.0,
            "currency": "MXN",
            "payment_method": "PUE",
            "status": "valid",
            "created_at": 1677651234,
            "stamp": {
                "stamp_at": 1677651234,
                "sello": "ABC123..."
            }
        }
    ],
    "has_more": false,
    "total_results": 1
}
```

### Create Income Invoice

```http theme={null}
POST /invoices/income
```

Create a new income invoice with optional automation.

**Automation Types:**

* `payment` - Create invoice with payment automation
* `none` - No automation, create invoice only

**Request Body:**

```json theme={null}
{
    "automation_type": "payment",
    "client": {
        "id": "client_1234567890"
    },
    "currency": "MXN",
    "exchange_rate": 1.0,
    "items": [
        {
            "description": "Consulting services",
            "quantity": 2,
            "unit_price": 1000.0,
            "product_key": "80141503",
            "unit_key": "E48",
            "unit_name": "Servicio",
            "taxes": [
                {
                    "type": "IVA",
                    "rate": 0.16,
                    "factor": "Tasa",
                    "withholding": false
                }
            ]
        }
    ],
    "use": "P01",
    "payment_form": "03",
    "payment_method": "PUE",
    "series": "A",
    "folio_number": 123,
    "send_email": true,
    "emails": ["client@example.com"]
}
```

**Example Request with Client Search:**

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/income \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "automation_type": "payment",
    "client": {
      "search": {
        "on_key": "tax_id",
        "on_value": "PEGJ800101ABC",
        "auto_create": true,
        "safety_check": false
      },
      "name": "Juan Perez Garcia",
      "email": "juan.perez@ejemplo.com",
      "tax_id": "PEGJ800101ABC",
      "tax_system": "601"
    },
    "currency": "MXN",
    "exchange_rate": 1.0,
    "items": [
      {
        "description": "Professional services",
        "quantity": 1,
        "unit_price": 5000.00,
        "product_key": "80141503",
        "unit_key": "E48",
        "taxes": [{"type": "IVA", "rate": 0.16}]
      }
    ],
    "use": "G03",
    "payment_form": "03",
    "payment_method": "PUE"
  }'
```

To issue many invoices at once, send the same bodies to [`POST /invoices/income/batch`](/guides/invoice-batches), 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:

| Situation | Answer | What to do |
| - | - | - |
| The invoice already exists | `400`, `message.code: "INVALID_INVOICE"`, `message.duplicate: true`, `message.uuid` | Treat it as success; read the invoice by its `uuid` |
| Another request with the key is still being processed | `409`, `error.code: "idempotency_in_progress"`, `retryable: true` | Retry later with the same key |
| The PAC's answer to an earlier attempt was lost | The retry resends **the same XML and folio**; the PAC stamps it once or returns the stamp it already made | Nothing; you get the invoice (`200`) or its error |
| The PAC can't confirm an earlier attempt | `409`, `message.code: "STAMP_NEEDS_REVIEW"` | Contact support. Don't retry under a new key |
| An earlier attempt was rejected (bad data, SAT rejection) | The key is free again; the request is processed normally | Fix the body and send it with the same key |

The duplicate answer looks like this:

```json theme={null}
{
    "message": {
        "error": "Error al timbrar la factura: Ya existe un comprobante con la misma idempotencia (uuid: 0f8fad5b-d9cb-469f-a165-70867728950e)",
        "code": "INVALID_INVOICE",
        "providerMessage": "Ya existe un comprobante con la misma idempotencia (uuid: 0f8fad5b-d9cb-469f-a165-70867728950e)",
        "retryable": false,
        "duplicate": true,
        "uuid": "0f8fad5b-d9cb-469f-a165-70867728950e"
    }
}
```

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](#503-—-pac-unavailable-or-outcome-unknown)).

### Get Income Invoice

```http theme={null}
GET /invoices/income/{id}
```

Retrieve a specific income invoice by ID.

**Example Request:**

```bash theme={null}
curl -X GET https://api.gigstack.io/v2/invoices/income/invoice_1234567890 \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Create Egress Invoice

```http theme={null}
POST /invoices/egress
```

Create a new egress invoice (credit note / nota de crédito).

**Request Body:**

```json theme={null}
{
    "automation_type": "none",
    "client": {
        "id": "client_1234567890"
    },
    "currency": "MXN",
    "exchange_rate": 1.0,
    "items": [
        {
            "description": "Credit for returned goods",
            "quantity": 1,
            "unit_price": 500.0,
            "product_key": "80141503",
            "unit_key": "E48",
            "taxes": [
                {
                    "type": "IVA",
                    "rate": 0.16
                }
            ]
        }
    ],
    "use": "G02",
    "payment_form": "03",
    "payment_method": "PUE",
    "related_documents": [
        {
            "relationship": "01",
            "documents": ["A1B2C3D4-0000-0000-0000-000000000000"]
        }
    ]
}
```

**`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

```http theme={null}
GET /invoices/egress
```

Retrieve a paginated list of egress invoices (credit notes) with filtering capabilities.

**Query Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `limit` | integer (1-100) | Number of results per page (default: 10) |
| `next` | string | Pagination cursor for next page |
| `team` | string | gigstack Connect: Target team ID |
| `order_by` | string | Field to sort by (e.g., `created_at`) |
| `sort` | string | Sort direction (`asc`, `desc`) |
| `created_gte` | integer | Filter by creation date (greater than or equal to timestamp) |
| `created_lte` | integer | Filter by creation date (less than or equal to timestamp) |
| `client_id` | string | Filter by the gigstack client ID (e.g., `client_id=client_xxx`) |
| `tax_id` | string | Filter by the client's tax ID / RFC (e.g., `tax_id=PEGJ800101ABC`) |
| `metadata.{key}` | string | Filter by metadata field using dot notation (e.g., `metadata.order_id=ORD-123`) |
| `metadata_{key}` | string | Filter by metadata field using underscore notation (e.g., `metadata_order_id=ORD-123`) |
| `page` | integer | Page number for pagination when using metadata filters (default: 1) |

**Metadata Filtering:**

You can filter egress invoices by any metadata key using either dot notation or underscore notation. Both formats are equivalent:

```bash theme={null}
# Dot notation
GET /invoices/egress?metadata.order_id=ORD-123

# Underscore notation (alternative)
GET /invoices/egress?metadata_order_id=ORD-123
```

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:**

```bash theme={null}
# Basic listing
curl -X GET "https://api.gigstack.io/v2/invoices/egress?limit=20" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Filter by creation date range
curl -X GET "https://api.gigstack.io/v2/invoices/egress?created_gte=1700000000000&created_lte=1710000000000" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Filter by metadata (dot notation)
curl -X GET "https://api.gigstack.io/v2/invoices/egress?metadata.expense_category=travel" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Filter by metadata (underscore notation)
curl -X GET "https://api.gigstack.io/v2/invoices/egress?metadata_vendor_id=VEND-789" \
  -H "Authorization: Bearer YOUR_TOKEN"

# Multiple metadata filters with pagination
curl -X GET "https://api.gigstack.io/v2/invoices/egress?metadata.department=IT&metadata.approved=true&page=2&limit=20" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Get Invoice Files

```http theme={null}
GET /invoices/{id}/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:**

```bash theme={null}
curl -X GET "https://api.gigstack.io/v2/invoices/invoice_1234567890/files?file_type=pdf" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

**Example Response:**

```json theme={null}
{
    "message": "Files retrieved successfully",
    "data": [
        { "content": "JVBERi0xLjcKJeLjz9MK...", "filename": "Yx7Kp2Lm9Qw4Rt6Bn1Vc.pdf", "type": "application/pdf" }
    ]
}
```

### Cancel Invoice

```http theme={null}
DELETE /invoices/{id}
```

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](/countries/colombia).

| `motive` | Meaning | Additional input |
| - | - | - |
| `01` | Issued with errors; a replacement CFDI exists | `substitution_uuid` of the replacement |
| `02` | Issued with errors; no replacement | Omit `substitution_uuid` |
| `03` | The operation did not take place | Omit `substitution_uuid` |
| `04` | A nominative sale was included in a global invoice | Follow the applicable global-invoice correction flow |

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](/guides/catalogs/cancellation_motives)
explains the codes.

Use the environment and key from the [quickstart](/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:

```bash theme={null}
curl --fail-with-body "$GIGSTACK_BASE_URL/invoices/$INVOICE_UUID" \
  -H "Authorization: Bearer $GIGSTACK_API_KEY" > invoice-before-cancel.json

jq '.data | {uuid, status, cancellation_status, livemode}' invoice-before-cancel.json

curl --fail-with-body -X DELETE "$GIGSTACK_BASE_URL/invoices/$INVOICE_UUID" \
  -H "Authorization: Bearer $GIGSTACK_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"motive":"02"}' > cancellation.json

jq '{cancellation_status, code, message}' cancellation.json
```

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.

| Result | Next action |
| - | - |
| Stored `status: "canceled"` with accepted cancellation | Retain the original document and cancellation evidence; do not treat it as deleted |
| `pending`, inconclusive or conflicting result | Keep it pending in your application; re-read with bounded backoff and contact support if unresolved |
| `403` mode mismatch | Use the key matching the invoice's stored mode |
| `422` imported external invoice | Cancel through the original stamping provider; gigstack cannot cancel that import |
| Timeout, `500` or `503` | Reconcile the invoice and provider result before another cancellation request |

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](#error-handling)
for provider errors.

### Create Payment Complement (Complemento de Pago)

```http theme={null}
POST /invoices/payment
```

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:**

| Field | Type | Required | Description |
| - | - | - | - |
| `client` | object | yes | Client reference (`{ "id": "client_…" }`) or inline client, as in the other invoice endpoints |
| `complements` | array | yes | Normally one entry: `{ "type": "pago", "data": [ …payments ] }`. `type` is optional and defaults to `pago` |
| `date` | integer | no | Comprobante date, epoch ms. Defaults to now |
| `series` | string | no | Overrides the payments series |
| `folio_number` | integer | no | Stamp with this exact folio |
| `related_documents` | array | no | CFDI relations at the comprobante level |
| `idempotency_key` | string | no | Prevents creating the same complement twice |
| `return_files` | boolean | no | Include base64 XML and PDF in the response |
| `send_email`, `ignore_emails`, `emails` | | no | Email delivery options |
| `invoice_pdf_notes`, `metadata` | | no | Notes printed on the PDF; your own key-value data |

**Each payment (`complements[].data[]`):**

| Field | Required | Description |
| - | - | - |
| `payment_form` | yes | SAT `c_FormaPago`, e.g. `03` (transfer) |
| `date` | yes | Payment date and time, ISO 8601 |
| `currency` | yes | Payment currency |
| `exchange` | no | Exchange rate to MXN (default 1) |
| `related_documents` | yes | The PPD invoices this payment pays |

**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:**

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/payment \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "client": { "id": "client_1234567890" },
    "complements": [{
      "type": "pago",
      "data": [{
        "payment_form": "03",
        "date": "2026-07-24T12:00:00",
        "currency": "MXN",
        "exchange": 1,
        "related_documents": [{
          "uuid": "A1B2C3D4-E5F6-7890-ABCD-1234567890AB",
          "amount": 116,
          "installment": 1,
          "last_balance": 116,
          "currency": "MXN",
          "taxes": [{ "base": 100, "rate": "0.16", "factor": "Tasa", "type": "IVA", "withholding": false, "inclusive": false }]
        }]
      }]
    }]
  }'
```

**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)

```http theme={null}
POST /invoices/transfer
GET  /invoices/transfer
GET  /invoices/transfer/{id}
```

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:**

| Field | Type | Required | Description |
| - | - | - | - |
| `client` | object | yes | Receptor, as in the other invoice endpoints. For goods you move yourself, this is usually your own company |
| `carta_porte` | object | yes | Carta Porte data, using the SAT attribute names from CartaPorte31.xsd (see below) |
| `series` | string | no | Defaults to `T` |
| `folio_number` | number | no | Custom folio |
| `date` | number | no | Unix epoch milliseconds |
| `idempotency_key` | string | no | Safe retries |
| `related_documents` | array | no | CFDI relations |
| `send_email`, `emails`, `metadata`, `return_files` | | no | Same as the other invoice endpoints |

**`carta_porte`** (numbers can be numbers or numeric strings):

| Field | Required | Notes |
| - | - | - |
| `TranspInternac` | yes | `No`, or `Sí` with `EntradaSalidaMerc` and `PaisOrigenDestino` |
| `TotalDistRec` | yes | Total km |
| `Ubicaciones[]` | yes, 2+ | `TipoUbicacion` (`Origen`/`Destino`), `RFCRemitenteDestinatario`, `FechaHoraSalidaLlegada` (`YYYY-MM-DDTHH:mm:ss`), `Domicilio` (`Estado`, `Pais`, `CodigoPostal` required, SAT keys). Every `Destino` needs `DistanciaRecorrida`; it is dropped from the `Origen` |
| `Mercancias` | yes | `PesoBrutoTotal`, `UnidadPeso` (e.g. `KGM`), `NumTotalMercancias`, `Mercancia[]` with `BienesTransp`, `Descripcion`, `Cantidad`, `ClaveUnidad`, `PesoEnKg` |
| `Autotransporte` | yes | `PermSCT`, `NumPermisoSCT`, `IdentificacionVehicular` (`ConfigVehicular`, `PlacaVM`, `AnioModeloVM`), `Seguros` (`AseguraRespCivil`, `PolizaRespCivil`), optional `Remolques[]` (max 2) |
| `FiguraTransporte[]` | yes | `TipoFigura`, `RFCFigura`; for `01` (operator) send `NumLicencia` |

**Example:**

```json theme={null}
{
    "client": { "id": "client_1234567890" },
    "carta_porte": {
        "TranspInternac": "No",
        "TotalDistRec": 120,
        "Ubicaciones": [
            {
                "TipoUbicacion": "Origen",
                "RFCRemitenteDestinatario": "EKU9003173C9",
                "NombreRemitenteDestinatario": "ESCUELA KEMPER URGATE",
                "FechaHoraSalidaLlegada": "2026-10-06T09:00:00",
                "Domicilio": { "Pais": "MEX", "CodigoPostal": "42501", "Estado": "HID" }
            },
            {
                "TipoUbicacion": "Destino",
                "RFCRemitenteDestinatario": "EKU9003173C9",
                "NombreRemitenteDestinatario": "ESCUELA KEMPER URGATE",
                "FechaHoraSalidaLlegada": "2026-10-06T13:00:00",
                "DistanciaRecorrida": 120,
                "Domicilio": { "Pais": "MEX", "CodigoPostal": "03020", "Estado": "CMX" }
            }
        ],
        "Mercancias": {
            "PesoBrutoTotal": 12.5,
            "UnidadPeso": "KGM",
            "NumTotalMercancias": 1,
            "Mercancia": [
                { "BienesTransp": "50202203", "Descripcion": "Bebida embotellada", "Cantidad": 10, "ClaveUnidad": "XBO", "PesoEnKg": 12.5 }
            ]
        },
        "Autotransporte": {
            "PermSCT": "TPAF01",
            "NumPermisoSCT": "0X2XTXZ0X5X0X3X2X1X0",
            "IdentificacionVehicular": { "ConfigVehicular": "VL", "PlacaVM": "ABC1234", "AnioModeloVM": 2022, "PesoBrutoVehicular": 3 },
            "Seguros": { "AseguraRespCivil": "SEGUROS SA", "PolizaRespCivil": "123456" }
        },
        "FiguraTransporte": [
            { "TipoFigura": "01", "RFCFigura": "VAAM130719H60", "NombreFigura": "OPERADOR", "NumLicencia": "a234567890" }
        ]
    }
}
```

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

```http theme={null}
GET /invoices/search?q=…
```

Full-text, typo-tolerant search across client name, email, invoice UUID, description and metadata.

| Parameter | Type | Description |
| - | - | - |
| `q` | string | **Required.** Search text (`query` is accepted as an alias; `q` wins) |
| `limit` | integer | Results per page (default 10, max 100) |
| `page` | integer | Page number (default 1) |
| `fields` | string | Comma-separated fields to include |

```bash theme={null}
curl "https://api.gigstack.io/v2/invoices/search?q=Juan%20P%C3%A9rez&limit=10" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

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

```http theme={null}
POST /invoices/{id}/support-documents
GET  /invoices/{id}/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`:

| Field | Required | Description |
| - | - | - |
| `file` | yes | PDF or image (PNG, JPG, WEBP), up to 10 MB |
| `documentType` | yes | `contract`, `delivery_proof`, `payment_proof`, `communication`, `payment_confirmation` or `subscription_info` |
| `name` | no | Display name |
| `description` | no | Free text |

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/invoice_1234567890/support-documents \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@contrato-acme.pdf" \
  -F "documentType=contract" \
  -F "name=Contrato de servicios 2026"
```

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](/guides/documents).

### End-of-Month Global Invoicing

```http theme={null}
POST /invoices/eom/run
```

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:

```json theme={null}
{
    "success": true,
    "message": "The end-of-month global invoicing process has been triggered successfully.",
    "data": { "global_invoice_time": "31/01/2026 23:59:00" },
    "timestamp": 1767225600000
}
```

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

```http theme={null}
POST /invoices/draft
```

Create a new draft invoice. Only `invoice_type` is required at creation; all other fields can be added later via update.

**Request Body:**

```json theme={null}
{
    "invoice_type": "I",
    "client": {
        "id": "client_1234567890"
    },
    "currency": "MXN",
    "items": [
        {
            "description": "Consulting services",
            "quantity": 1,
            "unit_price": 1000.00,
            "product_key": "80141503",
            "unit_key": "E48",
            "taxes": [{"type": "IVA", "rate": 0.16}]
        }
    ],
    "use": "G03",
    "payment_form": "03",
    "payment_method": "PUE"
}
```

**Minimal Request (just the type):**

```json theme={null}
{
    "invoice_type": "I"
}
```

**Example Request:**

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/draft \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_type": "I",
    "client": {"id": "client_1234567890"},
    "currency": "MXN",
    "items": [{
      "description": "Web development",
      "quantity": 10,
      "unit_price": 2500.00,
      "product_key": "81111500",
      "unit_key": "HUR",
      "taxes": [{"type": "IVA", "rate": 0.16}]
    }],
    "use": "G03",
    "payment_form": "03",
    "payment_method": "PUE"
  }'
```

**Response (201):**

```json theme={null}
{
    "message": "Draft created successfully",
    "data": {
        "id": "draft_abc123",
        "draft": true,
        "invoice_type": "I",
        "client": {
            "id": "client_1234567890",
            "name": "Juan Perez Garcia"
        },
        "currency": "MXN",
        "items": [...],
        "status": "draft",
        "created_at": 1709090576567
    }
}
```

### List Drafts

```http theme={null}
GET /invoices/draft
```

Retrieve a paginated list of draft invoices.

**Query Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `limit` | integer (1-100) | Number of results per page (default: 10) |
| `next` | string | Pagination cursor for next page |
| `invoice_type` | string | Filter by invoice type: `I` (income) or `E` (egress) |
| `client_id` | string | Filter by client ID |

**Example Request:**

```bash theme={null}
curl -X GET "https://api.gigstack.io/v2/invoices/draft?limit=20&invoice_type=I" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Get Draft

```http theme={null}
GET /invoices/draft/{id}
```

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

**Example Request:**

```bash theme={null}
curl -X GET https://api.gigstack.io/v2/invoices/draft/draft_abc123 \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Update Draft

```http theme={null}
PUT /invoices/draft/{id}
```

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](/recipes/invoice), create a complete revised request:

```bash theme={null}
jq '. + {invoice_pdf_notes: "Purchase order PO-1042"}' \
  draft-body.json > revised-draft-body.json

curl --fail-with-body -X PUT "$GIGSTACK_BASE_URL/invoices/draft/$DRAFT_ID" \
  -H "Authorization: Bearer $GIGSTACK_API_KEY" \
  -H 'Content-Type: application/json' \
  --data @revised-draft-body.json > updated-draft.json

curl --fail-with-body "$GIGSTACK_BASE_URL/invoices/draft/$DRAFT_ID" \
  -H "Authorization: Bearer $GIGSTACK_API_KEY" > draft-check.json

jq '.data | {id, invoice_type, client, items, currency, total, livemode}' draft-check.json
```

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

```http theme={null}
DELETE /invoices/draft/{id}
```

Permanently delete a draft and its associated preview files.

**Example Request:**

```bash theme={null}
curl -X DELETE https://api.gigstack.io/v2/invoices/draft/draft_abc123 \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### 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.

```http theme={null}
POST /invoices/draft/{id}/stamp
```

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:**

```json theme={null}
{
    "send_email": true,
    "return_files": true
}
```

* `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:**

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/draft/draft_abc123/stamp \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"return_files": true}'
```

**Response (200):**

```json theme={null}
{
    "message": "Invoice created from draft",
    "data": {
        "uuid": "12345678-1234-1234-1234-123456789012",
        "folio_number": 456,
        "series": "A",
        "total": 29000.00,
        "status": "valid",
        "stamp": {
            "stamp_at": 1709090576567,
            "sello": "ABC123..."
        },
        "files": {
            "xml": "PD94bWwgdmVyc2lvbj...",
            "pdf": "JVBERi0xLjQK..."
        }
    }
}
```

**Error: Incomplete draft (400):**

```json theme={null}
{
    "message": "Draft is incomplete — a client and at least one item are required to stamp"
}
```

**Error: Credit limit reached (429):**

```json theme={null}
{
    "message": "Team credit limit reached",
    "error": "No remaining credits",
    "credit_limit": 100,
    "used_credits": 100
}
```

### Preview Draft

```http theme={null}
POST /invoices/draft/{id}/preview
```

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:**

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/draft/draft_abc123/preview \
  -H "Authorization: Bearer YOUR_TOKEN"
```

**Response (200):**

```json theme={null}
{
    "message": "Preview PDF generated",
    "data": {
        "pdf": "JVBERi0xLjQK..."
    }
}
```

### Draft Workflow Example

Follow [Draft, preview, and issue](/recipes/invoice) 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](#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](/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)

| Code | Description |
| - | - |
| **01** | Efectivo |
| **02** | Cheque nominativo |
| **03** | Transferencia electronica de fondos |
| **04** | Tarjeta de credito |
| **28** | Tarjeta de debito |
| **99** | Por definir |

## Complex Invoice Examples

### Invoice with Multiple Items and Taxes

```json theme={null}
{
    "automation_type": "payment",
    "client": {
        "id": "client_1234567890"
    },
    "currency": "MXN",
    "exchange_rate": 1.0,
    "items": [
        {
            "description": "Consulting services - Phase 1",
            "quantity": 10,
            "unit_price": 1000.0,
            "product_key": "80141503",
            "unit_key": "HUR",
            "taxes": [
                {
                    "type": "IVA",
                    "rate": 0.16,
                    "withholding": false
                }
            ]
        },
        {
            "description": "Software license",
            "quantity": 1,
            "unit_price": 5000.0,
            "product_key": "81111500",
            "unit_key": "H87",
            "taxes": [
                {
                    "type": "IVA",
                    "rate": 0.16,
                    "withholding": false
                }
            ]
        }
    ],
    "use": "G03",
    "payment_form": "03",
    "payment_method": "PUE"
}
```

### Invoice with Withholding Taxes

```json theme={null}
{
    "automation_type": "payment",
    "client": {
        "id": "client_1234567890"
    },
    "currency": "MXN",
    "exchange_rate": 1.0,
    "items": [
        {
            "description": "Professional services",
            "quantity": 1,
            "unit_price": 10000.0,
            "product_key": "80141503",
            "unit_key": "E48",
            "taxes": [
                {
                    "type": "IVA",
                    "rate": 0.16,
                    "withholding": false
                },
                {
                    "type": "ISR",
                    "rate": 0.1,
                    "withholding": true
                },
                {
                    "type": "IVA",
                    "rate": 0.106667,
                    "withholding": true
                }
            ]
        }
    ],
    "use": "G03",
    "payment_form": "03",
    "payment_method": "PUE"
}
```

### PPD Invoice (Partial Payments)

```json theme={null}
{
    "automation_type": "none",
    "client": {
        "id": "client_1234567890"
    },
    "currency": "MXN",
    "exchange_rate": 1.0,
    "items": [
        {
            "description": "Large project - Total amount",
            "quantity": 1,
            "unit_price": 100000.0,
            "product_key": "80141503",
            "unit_key": "E48",
            "taxes": [
                {
                    "type": "IVA",
                    "rate": 0.16
                }
            ]
        }
    ],
    "use": "G03",
    "payment_form": "99",
    "payment_method": "PPD"
}
```

### Global Invoice

```json theme={null}
{
    "automation_type": "none",
    "client": {
        "legal_name": "PUBLICO EN GENERAL",
        "tax_id": "XAXX010101000",
        "tax_system": "616",
        "address": { "zip": "06600" }
    },
    "currency": "MXN",
    "exchange_rate": 1.0,
    "global": {
        "periodicity": "04",
        "months": "01",
        "year": 2024
    },
    "items": [
        {
            "description": "Ventas del periodo",
            "quantity": 1,
            "unit_price": 50000.0,
            "product_key": "01010101",
            "unit_key": "ACT",
            "taxes": [
                {
                    "type": "IVA",
                    "rate": 0.16
                }
            ]
        }
    ],
    "use": "S01",
    "payment_form": "01",
    "payment_method": "PUE"
}
```

The receiver of a global invoice is the general public (`XAXX010101000`, regime `616`, use `S01`, your own postal code). See [Global Invoices](/guides/catalogs/invoices_globals) for the periodicity and month codes.

## Related Documents

### Creating Related Invoices

```json theme={null}
{
    "related_documents": [
        {
            "relationship": "04",
            "documents": ["12345678-1234-1234-1234-123456789012"]
        }
    ]
}
```

**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

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/income \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "automation_type": "payment",
    "client_id": "client_1234567890",
    "currency": "MXN",
    "exchange_rate": 1.0,
    "items": [{
      "description": "Product sale",
      "quantity": 1,
      "unit_price": 1000.00,
      "product_key": "01010101",
      "unit_key": "H87",
      "taxes": [{"type": "IVA", "rate": 0.16}]
    }],
    "use": "G01",
    "payment_form": "03",
    "payment_method": "PUE"
  }'
```

### 2. Service Invoice with Email

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/income \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "automation_type": "payment",
    "client_id": "client_1234567890",
    "currency": "MXN",
    "exchange_rate": 1.0,
    "items": [{
      "id": "service_1234567890",
      "quantity": 1
    }],
    "use": "G03",
    "payment_form": "03",
    "payment_method": "PUE",
    "send_email": true,
    "emails": ["client@example.com", "accounting@example.com"]
  }'
```

### 3. USD Invoice with Exchange Rate

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/income \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "automation_type": "payment",
    "client_id": "client_1234567890",
    "currency": "USD",
    "exchange_rate": 18.50,
    "items": [{
      "description": "International services",
      "quantity": 1,
      "unit_price": 100.00,
      "product_key": "80141503",
      "unit_key": "E48",
      "taxes": [{"type": "IVA", "rate": 0.16}]
    }],
    "use": "G03",
    "payment_form": "03",
    "payment_method": "PUE",
    "exports": "02"
  }'
```

## 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.

## Related Resources

* [CFDI Errors Reference](/guides/catalogs/cfdi_errors) - Comprehensive error code catalog
* [Clients API](/guides/clients) - Manage invoice recipients
* [Services API](/guides/services) - Configure invoice items
* [Payments API](/guides/payments) - Process invoice payments
* [Teams API](/guides/teams) - Configure invoice settings

## Error Handling

When working with invoices, you may encounter various CFDI-specific error codes. Use the [CFDI Errors endpoint](/guides/catalogs/cfdi_errors) to look up detailed explanations and solutions for any error codes you receive.

### At a glance

| Status | When | Retry the same request? | What you should do |
| - | - | - | - |
| `400` | Body validation, invalid currency, or the **SAT rejected the document's data** | No — it will fail identically | Fix the payload. Read `code` and look it up in the [CFDI errors catalog](/guides/catalogs/cfdi_errors). |
| `401` | Missing/invalid token | No | Re-authenticate. |
| `403` | Livemode mismatch, or the resource belongs to another team | No | Use a key in the matching mode, or the right `?team=`. |
| `404` | Invoice, draft, client or service not found | No | Check the id. |
| `409` | A concurrent request is creating the same client or service (`search` + `auto_create`), or another request with the same `idempotency_key` is in progress (`idempotency_in_progress`) | **Yes**, after a short backoff | Wait and retry; the other request is mid-flight. |
| `409` | `STAMP_NEEDS_REVIEW`: the PAC can't confirm whether an earlier attempt with this `idempotency_key` stamped | No | Contact support. See below. |
| `412` | **SAT not connected** / CSD not valid | No, until fixed | Finish the SAT connection. See below. |
| `422` | The document cannot be operated on at all — e.g. cancelling an imported invoice | No | See below. |
| `429` | **Team credit limit reached** | No, until raised | See below. |
| `500` | Unhandled server error | Maybe | Report with the `[pcs_…]` process id if present. |
| `502` | An upstream service returned an error (e.g. the PDF renderer) | Yes, with backoff | See below. |
| `503` | `PAC_UNAVAILABLE`: **the PAC was unavailable**, the document was never evaluated | **Yes**, with backoff | See below. |
| `503` | `PAC_OUTCOME_UNKNOWN`: **the PAC's answer was lost**, the CFDI may exist | **Only with an `idempotency_key`** (`retryable: true`) | See below. |

### Two error shapes

CFDI failures use a dedicated shape:

```json theme={null}
{
    "error": "Human-readable message, in Spanish, ending in an optional [pcs_…] process id",
    "code": "CFDI40147",
    "providerMessage": "The PAC's untouched original text",
    "retryable": false
}
```

* `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](#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:

```json theme={null}
{
    "success": false,
    "error": { "code": "invalid_request_body", "message": "…", "details": ["…"] },
    "timestamp": 1718451000000
}
```

### 412 — SAT not connected

```json theme={null}
{
    "message": {
        "error": "Your SAT connection is not finished yet. Go to https://app.gigstack.pro/integrations and complete the SAT connection (Invoicing section) before issuing invoices.",
        "code": "SAT_NOT_CONNECTED",
        "retryable": false
    }
}
```

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`**

```json theme={null}
{
    "message": {
        "error": "El servicio de timbrado no está disponible en este momento. Vuelve a intentarlo en unos minutos. …",
        "code": "PAC_UNAVAILABLE",
        "retryable": true
    }
}
```

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`**

```json theme={null}
{
    "message": {
        "error": "No se recibió la respuesta del servicio de timbrado; el comprobante podría estar timbrado. …",
        "code": "PAC_OUTCOME_UNKNOWN",
        "retryable": true
    }
}
```

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

```json theme={null}
{
    "message": {
        "error": "El PAC indica que el comprobante A-1284 ya fue timbrado pero no devolvió su UUID: … [pcs_…]",
        "code": "STAMP_NEEDS_REVIEW",
        "retryable": false
    }
}
```

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

```json theme={null}
{
    "message": "Team credit limit reached",
    "error": "Team credit limit reached (500/500)",
    "credit_limit": 500,
    "used_credits": 500
}
```

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

```json theme={null}
{
    "message": "Imported invoices stamped by an external PAC cannot be canceled through gigstack. Please cancel through your original PAC provider."
}
```

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

```json theme={null}
{
    "success": false,
    "message": "PDF generator 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

```json theme={null}
{
    "success": false,
    "error": {
        "code": "resource_conflict",
        "message": "Client creation in progress for tax_id=… . Please retry."
    }
}
```

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

```json theme={null}
{
    "message": "An invoice with this idempotency_key is already being created",
    "error": {
        "code": "idempotency_in_progress",
        "message": "An invoice with this idempotency_key is already being created; retry later"
    },
    "retryable": true
}
```

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)

```json theme={null}
{
    "message": "Invalid body",
    "errors": ["client: is required"]
}
```

### Cancellation Error

Note the flat shape here — `DELETE /invoices/{id}` does **not** nest the CFDI error under `message`:

```json theme={null}
{
    "error": "Error al timbrar la factura: …",
    "code": "CANCEL_ERROR",
    "providerMessage": "…",
    "retryable": false
}
```

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](https://docs.gigstack.io) or contact [support@gigstack.io](mailto:support@gigstack.io).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.