Skip to main content
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.

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. 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, until status is completed.
  3. Read the results. Check 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)
  • 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)
  • 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

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, and the receipts nobody invoiced are included in the end-of-month global invoice automatically (see End-of-Month Global Invoicing and Global Invoices). Use a batch for the customers who do need their own CFDI. Not supported by batches:

Idempotency

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

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

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.

Item Status

Endpoints

Create a Batch

Headers: Body: { "invoices": [ … ] }, 1 to 1,000 items. Each item is the POST /invoices/income body with a required idempotency_key. livemode, team and owner in an item are ignored; they come from the credential.
Response (202):
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. 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

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.
A finished batch in which a few invoices failed:
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

One entry per accepted item, in request order. Rejected items aren’t listed; they are in the batch’s rejected.
Response (200): the array is at data.data and the cursor at data.next.
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. Batches add a few of their own: 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}, and GET /invoices/{id}/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 to invoice_batch.completed. It is sent once, when the batch completes, and carries the batch id, its counts and its result:
The delivery is signed with the webhook’s secret (see 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.

Response Objects

Batch

Item

Error Handling

The batch endpoints use the standardized envelope. Branch on error.code, not on the message.
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. 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.

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.

For additional assistance, contact support@gigstack.io