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

# Invoice Batches API Guide

> Integration guide for Invoice Batches

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

## Overview

An income invoice batch issues up to **1,000** CFDIs de ingreso from a single request. Each item of the batch is exactly the body you would send to [`POST /invoices/income`](/guides/invoices#create-income-invoice). gigstack validates every item right away, answers `202` with the batch, and stamps the accepted items in the background.

Use it when your company issues **one invoice per sale of its own** and has many of them at once: the day's orders from a store, a monthly subscription run, or a backlog after an outage.

Base path: `https://api.gigstack.io/v2/invoices/income/batch`

A batch goes through three steps:

1. **Create.** `POST /invoices/income/batch` with an `Idempotency-Key` header and `{ "invoices": [ … ] }`. Items that fail validation are listed in `rejected`; the rest are accepted.
2. **Follow.** Poll `GET /invoices/income/batch/{id}`, or subscribe a webhook to [`invoice_batch.completed`](#following-progress), until `status` is `completed`.
3. **Read the results.** Check [`result`](#batch-result), then page through `GET /invoices/income/batch/{id}/items` for each invoice's `uuid` or `error`.

## Key Features

* **Up to 1,000 invoices per request** - send more requests for more (see [Limits](#limits))
* **The single endpoint's rules, unchanged** - each item is validated and stamped by the same code as `POST /invoices/income`, so it succeeds or fails exactly as a single call would
* **Item-level rejections** - an invalid item is rejected with a reason, and the other items still go ahead
* **Safe retries at two levels** - the `Idempotency-Key` header makes a retried request return the same batch, and each item's `idempotency_key` makes sure an invoice is never issued twice (see [Idempotency](#idempotency))
* **Automatic retries** - an item that hits a temporary PAC failure is retried by gigstack, up to 6 attempts
* **A clear outcome** - a finished batch reports `result`: `completed`, `partially_completed` or `failed`
* **Polling or webhook** - `GET /invoices/income/batch/{id}` at any time, or `invoice_batch.completed` when it finishes

## When to Use It

| You want to… | Use |
| - | - |
| Issue one invoice now, and show the result to the person waiting for it | [`POST /invoices/income`](/guides/invoices#create-income-invoice). It answers with the stamped invoice |
| Issue dozens to thousands of invoices, where a few minutes' delay is fine | `POST /invoices/income/batch` |
| Invoice sales to the general public (*público en general*) who didn't ask for a CFDI | Usually neither. See below |

**Sales to the general public.** If most of your sales go to customers who don't ask for an invoice, you may not need one CFDI per sale at all. gigstack already produces the monthly **global invoice** (*factura global*) that groups those sales: record each sale as a [receipt](/guides/receipts), and the receipts nobody invoiced are included in the end-of-month global invoice automatically (see [End-of-Month Global Invoicing](/guides/invoices#end-of-month-global-invoicing) and [Global Invoices](/guides/catalogs/invoices_globals)). Use a batch for the customers who do need their own CFDI.

**Not supported by batches:**

* File uploads (CSV/XLSX). The batch takes JSON only.
* Egress invoices (*notas de crédito*) and payment complements (*complementos de pago*). Use [`POST /invoices/egress`](/guides/invoices#create-egress-invoice) and [`POST /invoices/payment`](/guides/invoices#create-payment-complement-complemento-de-pago) one by one.
* Returning files. `return_files` in an item is ignored; download the files later with [`GET /invoices/{id}/files`](/guides/invoices#get-invoice-files).

## Idempotency

A batch uses two keys, and they protect different things.

| Key | Where | Names | Protects against |
| - | - | - | - |
| `Idempotency-Key` | Request header, required | The **batch** | A retried request creating a second batch |
| `idempotency_key` | Each item, required | The **invoice** | The same invoice being issued twice, whichever request it came from |

### The batch key (`Idempotency-Key` header)

8-128 characters from `A-Z a-z 0-9 . _ : -`, for example `sales-2026-09-29-part-1`. The batch id is derived from your team, the credential's mode and this key, so:

* The **first** request with a key creates the batch: `202`.
* A later request with the **same key and the same body** returns that batch as it is now: `200`. Nothing is created again. If the first request died halfway through creating the batch, the retry finishes creating it.
* The **same key with a different body** is refused: `409 idempotency_key_reused`. Bodies are compared as sent, **including the order of keys**, so a retry must resend the exact same JSON.
* The same key used with a test key and with a live key names two different batches.

So if a `POST` times out or answers `500`, send it again unchanged with the same key.

### The invoice key (`idempotency_key` in each item)

This is the same `idempotency_key` that [`POST /invoices/income`](/guides/invoices#idempotency-and-safe-retries) accepts, for example your order id. It must be present in every item and unique within the batch (up to 256 characters; surrounding spaces are trimmed).

gigstack claims the key before it charges a credit or stamps, so:

* An invoice already issued under the key, by an earlier batch or by a single `POST /invoices/income`, is **not issued again**. The item ends `duplicate`, with the existing invoice's `uuid`, and no credit is charged.
* If the PAC's answer is lost, the next attempt sends the **same XML with the same folio**. The PAC either stamps it then, or reports the stamp it already made. It is never stamped twice. When the PAC cannot say, the item ends `needs_review` instead of risking a second CFDI.
* A rejection by the SAT frees the key, so you can fix the data and send the invoice again under the same key.

This is what makes it safe to **send a new batch with the items that failed**, or even to resend a whole batch under a new `Idempotency-Key`: everything already issued comes back as `duplicate`.

Keys are scoped to your team and to the credential's mode.

## Limits

* **1,000 invoices per request.** More is `400 too_many_items`. For 5,000 invoices, send five batches, each with its own `Idempotency-Key`. There is no limit on the number of batches.
* **Body size.** Keep the JSON body under 10 MB.
* **Throughput.** A team's items are stamped about 10 at a time across all its batches, so several batches from one team don't go faster than one. Expect up to roughly 10,000 invoices an hour, and less when other teams are stamping batches at the same time.
* **Credits.** Each stamped item consumes one credit, as a single call would. When the team's limit is reached, the remaining items fail with `credit_limit_reached`.

## Batch Status

| `status` | Meaning | What to do |
| - | - | - |
| `processing` | Some accepted items are still `queued` | Keep polling, or wait for the webhook |
| `completed` | Every accepted item has a final status | Read `result`: this status says nothing about how many invoices were issued |

A batch whose items were all rejected up front has nothing to stamp. It completes at once, usually already in the `202` response, with `result: "failed"`, and still sends `invoice_batch.completed`.

## Batch Result

`status: "completed"` only means there is nothing left to do. To know how it went, **read `result`, not `status`**. It is `null` while the batch is `processing`.

| `result` | When |
| - | - |
| `completed` | Every item was issued (`stamped`, or `duplicate` because it had been issued before), and nothing was rejected |
| `partially_completed` | At least one item was issued, and at least one wasn't (`failed`, `needs_review`, or rejected up front) |
| `failed` | No item was issued |

## Item Status

| `status` | Meaning | What to do |
| - | - | - |
| `queued` | Waiting for, or in, a stamping attempt. An item whose attempt failed for a temporary reason stays `queued` while it waits to be retried | Nothing |
| `stamped` | Issued by this batch. `uuid` and `invoice_id` are set | Nothing |
| `duplicate` | Already issued under this `idempotency_key` before. `uuid` and `invoice_id` name that invoice. Nothing was issued or charged again | Treat it as issued |
| `failed` | Processing ended unsuccessfully; inspect `error`. An exhausted `PAC_OUTCOME_UNKNOWN` does not prove no document exists | Reconcile uncertain outcomes and keep the **same** invoice `idempotency_key` on any supported retry |
| `needs_review` | The PAC could not confirm whether the invoice was stamped. gigstack never retries it, so it can't be issued twice | Contact support with the batch id and the item's `index`. Don't resend it under a new key |

## Endpoints

### Create a Batch

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

**Headers:**

| Header | Required | Description |
| - | - | - |
| `Authorization` | Yes | `Bearer YOUR_TOKEN` |
| `Idempotency-Key` | Yes | 8-128 characters from `A-Z a-z 0-9 . _ : -`. See [The batch key](#the-batch-key-idempotency-key-header) |
| `Content-Type` | Yes | `application/json` |

**Body:** `{ "invoices": [ … ] }`, 1 to 1,000 items. Each item is the [`POST /invoices/income` body](/guides/invoices#create-income-invoice) with a required `idempotency_key`. `livemode`, `team` and `owner` in an item are ignored; they come from the credential.

```bash theme={null}
curl -X POST https://api.gigstack.io/v2/invoices/income/batch \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Idempotency-Key: sales-2026-09-29-part-1" \
  -H "Content-Type: application/json" \
  -d '{
    "invoices": [
      {
        "idempotency_key": "order-2026-09-000123",
        "automation_type": "none",
        "client": { "id": "client_1234567890" },
        "currency": "MXN",
        "use": "G03",
        "payment_form": "04",
        "payment_method": "PUE",
        "items": [
          {
            "description": "Professional consulting services",
            "quantity": 1,
            "unit_price": 1000,
            "product_key": "80141503",
            "unit_key": "E48",
            "taxes": [{ "type": "IVA", "rate": 0.16, "factor": "Tasa", "withholding": false }]
          }
        ],
        "send_email": true
      },
      {
        "idempotency_key": "order-2026-09-000124",
        "automation_type": "none",
        "client": { "id": "client_0987654321" },
        "currency": "MXN",
        "use": "G03",
        "payment_form": "03",
        "payment_method": "PUE",
        "items": [
          {
            "description": "Annual support plan",
            "quantity": 1,
            "unit_price": 2500,
            "product_key": "81111811",
            "unit_key": "E48",
            "taxes": [{ "type": "IVA", "rate": 0.16, "factor": "Tasa", "withholding": false }]
          }
        ]
      }
    ]
  }'
```

**Response (202):**

```json theme={null}
{
    "success": true,
    "data": {
        "id": "ibatch_5d41402abc4b2a76b9719d911017c592",
        "object": "invoice_batch",
        "type": "income",
        "livemode": true,
        "status": "processing",
        "result": null,
        "total": 250,
        "accepted": 248,
        "rejected": [
            {
                "index": 17,
                "idempotency_key": "order-2026-09-000140",
                "error": { "code": "invalid_body", "message": "client_id: Unexpected field" }
            },
            {
                "index": 42,
                "idempotency_key": "order-2026-09-000123",
                "error": { "code": "duplicate_idempotency_key", "message": "idempotency_key is already used by item 0" }
            }
        ],
        "counts": { "queued": 248, "stamped": 0, "failed": 0, "duplicate": 0, "needs_review": 0 },
        "created_at": 1790780400000,
        "completed_at": null
    },
    "timestamp": 1790780401250
}
```

A retry with the same `Idempotency-Key` and body answers `200` with the same batch, as it is now.

#### Rejected items

Each item is checked on its own when the batch is created, with no lookups. A rejected item is never stamped or charged, and doesn't stop the others. `index` is its position in `invoices`, from 0.

| `error.code` | Why |
| - | - |
| `invalid_item` | The item isn't a JSON object |
| `idempotency_key_required` | No `idempotency_key`, or an empty one |
| `invalid_idempotency_key` | The key is longer than 256 characters |
| `duplicate_idempotency_key` | An earlier item of this batch has the same key. The message names that item's index |
| `invalid_body` | The item fails the `POST /invoices/income` validation. The message lists each `path: message` |

What needs the database or the PAC (a client id that doesn't exist, a SAT rejection) can't be known up front. Those items are accepted and end `failed`.

### Get a Batch

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

Returns the batch in the same shape as the create response. `counts` fills in as items finish, and `result` and `completed_at` are set when the batch completes.

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

A finished batch in which a few invoices failed:

```json theme={null}
{
    "success": true,
    "data": {
        "id": "ibatch_5d41402abc4b2a76b9719d911017c592",
        "object": "invoice_batch",
        "type": "income",
        "livemode": true,
        "status": "completed",
        "result": "partially_completed",
        "total": 250,
        "accepted": 248,
        "rejected": ["..."],
        "counts": { "queued": 0, "stamped": 245, "failed": 2, "duplicate": 1, "needs_review": 0 },
        "created_at": 1790780400000,
        "completed_at": 1790784000000
    },
    "timestamp": 1790784060000
}
```

A batch of another team, or of the other mode (a live batch read with a test key), answers `404 not_found`.

### List Batch Items

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

One entry per **accepted** item, in request order. Rejected items aren't listed; they are in the batch's `rejected`.

| Parameter | Type | Description |
| - | - | - |
| `limit` | integer | Items per page, 1-500 (default **100**). Anything else is `400 invalid_limit` |
| `next` | string | Cursor from the previous page's `data.next`. An invalid one is `400 invalid_cursor` |
| `status` | string | Only items in this status: `queued`, `stamped`, `failed`, `duplicate` or `needs_review`. Anything else is `400 invalid_status` |

```bash theme={null}
curl "https://api.gigstack.io/v2/invoices/income/batch/ibatch_5d41402abc4b2a76b9719d911017c592/items?status=failed" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

**Response (200):** the array is at `data.data` and the cursor at `data.next`.

```json theme={null}
{
    "success": true,
    "data": {
        "data": [
            {
                "index": 0,
                "idempotency_key": "order-2026-09-000123",
                "status": "stamped",
                "invoice_id": "B0C3E4F2-1A2B-4C5D-9E8F-0123456789AB",
                "uuid": "B0C3E4F2-1A2B-4C5D-9E8F-0123456789AB",
                "error": null,
                "attempts": 1
            },
            {
                "index": 2,
                "idempotency_key": "order-2026-09-000125",
                "status": "failed",
                "invoice_id": null,
                "uuid": null,
                "error": {
                    "code": "CFDI40147",
                    "message": "Error al timbrar la factura: CFDI40147 - El campo UsoCFDI no es válido [pcs_8f2a1c]"
                },
                "attempts": 1
            }
        ],
        "next": "2",
        "has_more": true
    },
    "timestamp": 1790781000000
}
```

Pass `data.next` back as `next` while `data.has_more` is `true`. The cursor is the last `index` of the page, so pages never overlap or skip items while statuses change.

#### Item errors

`error` is set only on `failed` and `needs_review` items. Its `code` is what `POST /invoices/income` would have answered for the same body: a SAT/PAC code such as `CFDI40147`, or one of gigstack's (`SAT_NOT_CONNECTED`, `CSD_VALIDATION_ERROR`, `PAC_UNAVAILABLE`, `PAC_OUTCOME_UNKNOWN`, `STAMP_NEEDS_REVIEW`, …). Look it up in the [CFDI errors catalog](/guides/catalogs/cfdi_errors). Batches add a few of their own:

| `error.code` | Why | What to do |
| - | - | - |
| `credential_revoked` | The API key that created the batch was revoked or disabled before this item ran. The remaining items fail the same way | Send the failed items in a new batch with a valid key |
| `credit_limit_reached` | The team's credit limit was reached | Raise the limit, then send the failed items again |
| `http_<status>` | The invoice endpoint answered that status without a code, for example `http_404` for a client or service id that doesn't exist | Read `message`, fix, and send again |
| `auth_unavailable`, `internal_error`, `idempotency_in_progress`, `PAC_UNAVAILABLE`, `PAC_OUTCOME_UNKNOWN` | A temporary failure that lasted through all 6 attempts | Send the item again later with the same `idempotency_key` |

`message` is at most 500 characters. Stamping messages are in Spanish, as on the single endpoint.

**Finding the invoices.** `invoice_id` reads the invoice with [`GET /invoices/income/{id}`](/guides/invoices#get-income-invoice), and [`GET /invoices/{id}/files`](/guides/invoices#get-invoice-files) returns its PDF and XML. You can also find an invoice by your own key with `GET /invoices/income?idempotency_key=…`.

## Following Progress

**Polling.** Poll `GET /invoices/income/batch/{id}` every 30-60 seconds. `counts.queued` goes down to `0` as items finish; the batch is done when `status` is `completed`.

**Webhook.** Subscribe a [webhook](/guides/webhooks) to `invoice_batch.completed`. It is sent once, when the batch completes, and carries the batch id, its counts and its `result`:

```json theme={null}
{
    "id": "evt_9a2b7c4d1e6f3a8b",
    "event": "invoice_batch.completed",
    "created_at": 1790784000,
    "data": {
        "id": "ibatch_5d41402abc4b2a76b9719d911017c592",
        "livemode": true,
        "total": 250,
        "accepted": 248,
        "rejected": 2,
        "counts": { "queued": 0, "stamped": 245, "failed": 2, "duplicate": 1, "needs_review": 0 },
        "result": "partially_completed"
    }
}
```

The delivery is signed with the webhook's `secret` (see [Verifying Signatures](/guides/webhooks#verifying-signatures)), but it is sent **only once and never retried**. If your endpoint is down at that moment, you miss it. Use the webhook to react quickly, and keep a slow poll (for example every 10 minutes) as a fallback. Note that `data.rejected` is a **count** here, while on the batch object `rejected` is the list.

Each invoice the batch stamps also sends the usual `invoice.created` event, like any other invoice. Items that fail don't send `invoice.failed`, so read the items to find them.

## End-to-End Example

Use Bash, `curl` and `jq`, with `GIGSTACK_API_KEY` loaded locally and the input files
reviewed for the intended team and mode. These scripts do not retry failed writes
automatically. An HTTP or JSON error stops the script; reconcile any ambiguous write
before starting again.

```bash theme={null}
set -euo pipefail
TOKEN="${GIGSTACK_API_KEY:?Load your API key locally first}"
BASE="https://api.gigstack.io/v2/invoices/income/batch"
KEY="sales-2026-09-29-part-1"

# 1. Create the batch. If this times out, run it again unchanged: same key, same body.
BATCH_ID=$(curl --fail-with-body --silent --show-error -X POST "$BASE" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  --data-binary @invoices.json | jq -er '.data.id | select(type == "string" and length > 0)')

# 2. Items rejected up front: fix them and send them in another batch.
curl --fail-with-body --silent --show-error "$BASE/$BATCH_ID" -H "Authorization: Bearer $TOKEN" | jq '.data.rejected'

# 3. Wait until the batch completes (or react to the invoice_batch.completed webhook).
STATUS=""
for attempt in {1..120}; do
  BATCH=$(curl --fail-with-body --silent --show-error "$BASE/$BATCH_ID" -H "Authorization: Bearer $TOKEN")
  echo "$BATCH" | jq -c '.data.counts'
  STATUS=$(echo "$BATCH" | jq -er '.data.status')
  [ "$STATUS" = "completed" ] && break
  [ "$STATUS" = "processing" ] || { echo "Unexpected batch state" >&2; exit 1; }
  sleep 60
done
[ "$STATUS" = "completed" ] || { echo "Polling deadline reached; save the batch ID and reconcile" >&2; exit 1; }
echo "$BATCH" | jq -r '.data.result'   # completed, partially_completed or failed

# 4. List failed items. An unknown provider outcome does not prove no invoice exists.
# Also inspect needs_review separately; never resubmit it under a new invoice key.
NEXT=""
while true; do
  PAGE=$(curl --fail-with-body --silent --show-error "$BASE/$BATCH_ID/items?status=failed&limit=500${NEXT:+&next=$NEXT}" -H "Authorization: Bearer $TOKEN")
  echo "$PAGE" | jq -r '.data.data[] | "\(.index)\t\(.idempotency_key)\t\(.error.code)\t\(.error.message)"'
  [ "$(echo "$PAGE" | jq -r '.data.has_more')" = "true" ] || break
  NEXT=$(echo "$PAGE" | jq -r '.data.next')
done

# 5. Fix those invoices and send them in a new batch (new Idempotency-Key),
#    keeping each invoice's own idempotency_key so nothing can be issued twice.
```

## Response Objects

### Batch

| Field | Type | Description |
| - | - | - |
| `id` | string | `ibatch_` + 32 hex characters. Derived from team, mode and `Idempotency-Key` |
| `object` | string | Always `invoice_batch` |
| `type` | string | Always `income` |
| `livemode` | boolean | Mode of the credential that created the batch |
| `status` | string | `processing` or `completed`. See [Batch Status](#batch-status) |
| `result` | string \| null | `completed`, `partially_completed` or `failed` once completed, `null` before. See [Batch Result](#batch-result) |
| `total` | integer | Invoices in the request |
| `accepted` | integer | Invoices that passed validation and are processed |
| `rejected` | object\[] | `{ index, idempotency_key, error: { code, message } }` for each item refused up front. See [Rejected items](#rejected-items) |
| `counts` | object | Accepted items by status: `queued`, `stamped`, `failed`, `duplicate`, `needs_review`. They add up to `accepted` |
| `created_at` | integer | Epoch ms |
| `completed_at` | integer \| null | When the last item finished, epoch ms |

### Item

| Field | Type | Description |
| - | - | - |
| `index` | integer | Position in the request's `invoices`, from 0 |
| `idempotency_key` | string | The item's key, trimmed |
| `status` | string | See [Item Status](#item-status) |
| `invoice_id` | string \| null | For `GET /invoices/income/{id}`. Set for `stamped` and `duplicate` |
| `uuid` | string \| null | Folio fiscal. Set for `stamped` and `duplicate` |
| `error` | object \| null | `{ code, message }` for `failed` and `needs_review`. See [Item errors](#item-errors) |
| `attempts` | integer | Stamping attempts so far |

## Error Handling

The batch endpoints use the standardized envelope. **Branch on `error.code`**, not on the message.

```json theme={null}
{
    "success": false,
    "error": {
        "code": "idempotency_key_reused",
        "message": "This Idempotency-Key was already used with a different body. Use a new key for a different batch."
    },
    "timestamp": 1790780400000
}
```

Authentication failures (`401`, and the `403` for a revoked key or a plan without API access) come from the authentication layer with a raw `{ "message": … }` body; see [Authentication errors](/guides/welcome#authentication-errors).

| Status | `error.code` | Endpoints | When |
| - | - | - | - |
| `400` | `invalid_request_body` | `POST /invoices/income/batch` | `Idempotency-Key` missing or malformed |
| `400` | `invalid_body` | `POST /invoices/income/batch` | The body isn't `{ "invoices": [ … ] }` with at least one item |
| `400` | `too_many_items` | `POST /invoices/income/batch` | More than 1,000 items |
| `400` | `invalid_limit` | `GET /{id}/items` | `limit` isn't an integer 1-500 |
| `400` | `invalid_status` | `GET /{id}/items` | `status` isn't one of the item statuses |
| `400` | `invalid_cursor` | `GET /{id}/items` | `next` isn't a valid cursor |
| `401` | `unauthorized` | All | Missing or invalid credential |
| `403` | `forbidden` | `POST /invoices/income/batch` | User-scoped token (MCP, dashboard) without `editor` permission on invoices |
| `404` | `not_found` | `GET /{id}`, `GET /{id}/items` | No such batch in your team and mode |
| `409` | `idempotency_key_reused` | `POST /invoices/income/batch` | The `Idempotency-Key` was already used with a different body. Use a new key |
| `500` | `internal_server_error` | All | Unexpected failure. Retrying the `POST` with the same key and body is safe |

A problem with **one item** is never an HTTP error: it is in `rejected`, or on the item as `failed`.

**gigstack Connect.** A master team's API key with the `multipleIssuerAccounts` feature can create a batch for a connected team with `?team=<team id>`. The batch belongs to that team: read it and its items with the same `team` parameter, or they answer `404`. See [gigstack Connect](/guides/gigstack-connect).

## Best Practices

1. **Use your order id as each item's `idempotency_key`.** It is what guarantees an order is invoiced once, across batches, retries and single calls.
2. **Retry a request with the same `Idempotency-Key` and the exact same body.** Use a new key only for a different set of invoices.
3. **Resend failed items in a new batch, with their original `idempotency_key`.** Don't mint new keys for them, or you lose the protection against issuing twice.
4. **Never resend a `needs_review` item under a new key.** Contact support; it may already be stamped.
5. **Split large volumes into batches of up to 1,000.** Send them one after another; a team's items are stamped at the same pace however many batches it has.
6. **Try it with a test key first.** Test batches and live batches are separate.
7. **Read `result`, not `status`,** and list the `failed` items when it isn't `completed`.
8. **Don't rely on the webhook alone.** It is sent once and never retried; keep a slow poll as a fallback.

## Related Resources

* [Invoices API](/guides/invoices) - The single `POST /invoices/income`, whose body each item uses, and its error handling
* [Receipts API](/guides/receipts) - Sales that end up in the monthly global invoice
* [Webhooks API](/guides/webhooks) - Subscribe to `invoice_batch.completed`
* [Platform Payouts API](/guides/platform-payouts) - For marketplaces invoicing on behalf of their providers, not for your own sales
* [CFDI Errors Reference](/guides/catalogs/cfdi_errors) - Stamping errors you may see in an item's `error`
* [Test Mode](/guides/welcome#test-mode)

***

For additional assistance, contact [support@gigstack.io](mailto:support@gigstack.io)


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