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

# Payment Batches API Guide

> Integration guide for Payment Batches

## Overview

A payment batch registers up to **10,000** payments that you have already received from a single request. Each item of the batch is exactly the body you would send to [`POST /payments/register`](https://docs.gigstack.io/guides/payments#register-payment). gigstack validates every item right away, answers `202` with the batch, and registers the accepted items in the background, with the same automations a single call runs (`automation_type`, `ppd_invoice_id`).

Use it when you reconcile many payments at once: a day's bank statement, a settlement file from a processor gigstack does not connect to, or a backlog after an outage.

Base path: `https://api.gigstack.io/v2/payments/register/batch`

A batch goes through three steps:

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

It works like an [income invoice batch](https://docs.gigstack.io/guides/invoice-batches.md): same request shape, same two levels of idempotency, same status and result vocabulary. This guide repeats what you need and points there for the rest.

## Key Features

* **Up to 10,000 payments per request** - send more requests for more (see [Limits](#limits))
* **The single endpoint's rules, unchanged** - each item is validated and registered by the same code as `POST /payments/register`, so it succeeds or fails exactly as a single call would, and triggers the same invoice automations
* **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 a payment is never registered twice (see [Idempotency](#idempotency))
* **Automatic retries** - an item that hits a temporary 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 /payments/register/batch/{id}` at any time, or `payment_batch.completed` when it finishes

## When to Use It

| You want to… | Use |
| - | - |
| Register one payment now, and show the result to the person waiting for it | [`POST /payments/register`](https://docs.gigstack.io/guides/payments#register-payment). It answers with the payment |
| Register dozens to thousands of payments, where a few minutes' delay is fine | `POST /payments/register/batch` |
| Collect a payment that hasn't happened yet | [`POST /payments/request`](https://docs.gigstack.io/guides/payments#request-payment), one at a time |
| Split a payment with a connected team (`transfer_data`) | [`POST /payments/register`](https://docs.gigstack.io/guides/payments#payment-splitting-marketplace), one at a time. A batch item with `transfer_data` is rejected |

**Not supported by batches:**

* File uploads (CSV/XLSX). The batch takes JSON only. For a spreadsheet, see the Google Sheets import in the dashboard.
* Split payments (`transfer_data`). The item is rejected with `transfer_data_not_supported`.
* Payment requests, refunds and marking a request as paid. Those stay single calls.

## 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 **payment** | The same payment being registered twice, whichever request it came from |

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

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

* The same key with the **same body** answers `200` with the batch the first request created. Nothing is created again.
* The same key with a **different body** answers `409 idempotency_key_reused`. Nothing is created. Use a new key.
* Bodies are compared as sent, including key order, so resend the exact same JSON.

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

An invoice batch created under the same key is a different batch (`ibatch_…` against `pbatch_…`); the two never collide.

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

This is the same `idempotency_key` that [`POST /payments/register`](https://docs.gigstack.io/guides/payments#register-payment) accepts, for example your transaction or order id. It must be present in every item and unique within the batch (up to 256 characters; surrounding spaces are trimmed).

* A payment already registered under the key, by an earlier batch or by a single `POST /payments/register`, is **not registered again**. The item ends `duplicate`, with the existing payment's `payment_id`.
* A failure that keeps the item from being registered (a client that does not resolve, a date in the future) leaves the key free, so you can fix the data and send the payment 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 registered comes back as `duplicate`.

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

## Limits

* **10,000 payments per request.** More is `400 too_many_items`. For 15,000 payments, send two batches, each with its own `Idempotency-Key`. There is no limit on the number of batches.
* **Body size.** Keep the JSON body under 20 MB. A payment with an embedded client is about 1 KB, so 10,000 of them are a few MB.
* **Throughput.** A team's items run about 10 at a time across all its batches, invoice batches included, so several batches from one team don't go faster than one. A payment registers in well under a second. After a payment whose `automation_type` stamps an invoice (or that settles a PPD invoice), the worker pauses about a second so the stamping that follows arrives at a few per second: expect roughly 25,000 such payments an hour, and several times that for `automation_type: "none"`.
* **Credits.** Registering a payment consumes no credit. An invoice an item's `automation_type` creates is charged as it would be for a single call.

## 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 payments were registered |

A batch whose items were all rejected up front has nothing to do. It completes at once, usually already in the `202` response, with `result: "failed"`, and still sends `payment_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 registered (`registered`, or `duplicate` because it had been registered before), and nothing was rejected |
| `partially_completed` | At least one item was registered, and at least one wasn't (`failed`, or rejected up front) |
| `failed` | No item was registered |

## Item Status

| `status` | Meaning | What to do |
| - | - | - |
| `queued` | Waiting for, or in, a registration attempt. An item whose attempt failed for a temporary reason stays `queued` while it waits to be retried | Nothing |
| `registered` | Registered by this batch. `payment_id` is set | Nothing |
| `duplicate` | Already registered under this `idempotency_key` before. `payment_id` names that payment. Nothing was registered again | Treat it as registered |
| `failed` | Processing ended unsuccessfully; inspect `error` | Fix the data and send the item again, keeping the **same** `idempotency_key` |

## Endpoints

### Create a Batch

```http theme={null}
POST /payments/register/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:** `{ "payments": [ … ] }`, 1 to 10,000 items. Each item is the [`POST /payments/register` body](https://docs.gigstack.io/guides/payments#register-payment) 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/payments/register/batch \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Idempotency-Key: settlement-2026-10-22-part-1" \
  -H "Content-Type: application/json" \
  -d '{
    "payments": [
      {
        "idempotency_key": "txn-2026-10-000123",
        "automation_type": "pue_invoice",
        "client": { "id": "client_1234567890" },
        "currency": "MXN",
        "payment_form": "03",
        "date": 1790700000000,
        "items": [{ "id": "service_1234567890", "quantity": 1, "unit_price": 1000 }],
        "metadata": { "order_id": "ORD-000123" }
      },
      {
        "idempotency_key": "txn-2026-10-000124",
        "automation_type": "none",
        "client": { "id": "client_0987654321" },
        "currency": "MXN",
        "payment_form": "01",
        "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": "pbatch_7f3a9c1e5b2d4f6a8c0e2a4b6d8f0a1c",
        "object": "payment_batch",
        "livemode": true,
        "status": "processing",
        "result": null,
        "total": 120,
        "accepted": 119,
        "rejected": [
            {
                "index": 7,
                "idempotency_key": "txn-2026-10-000130",
                "error": { "code": "invalid_body", "message": "payment_form: Invalid enum value" }
            }
        ],
        "counts": { "queued": 119, "registered": 0, "failed": 0, "duplicate": 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 registered, and doesn't stop the others. `index` is its position in `payments`, 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 |
| `transfer_data_not_supported` | The item is a split payment. Register it with `POST /payments/register` |
| `invalid_body` | The item fails the `POST /payments/register` validation. The message lists each `path: message` |

What needs the database (a client or service id that doesn't exist, an exchange rate for the payment date) can't be known up front. Those items are accepted and end `failed`.

### Get a Batch

```http theme={null}
GET /payments/register/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/payments/register/batch/pbatch_7f3a9c1e5b2d4f6a8c0e2a4b6d8f0a1c \
  -H "Authorization: Bearer YOUR_TOKEN"
```

A finished batch in which one payment failed:

```json theme={null}
{
    "success": true,
    "data": {
        "id": "pbatch_7f3a9c1e5b2d4f6a8c0e2a4b6d8f0a1c",
        "object": "payment_batch",
        "livemode": true,
        "status": "completed",
        "result": "partially_completed",
        "total": 120,
        "accepted": 119,
        "rejected": ["..."],
        "counts": { "queued": 0, "registered": 117, "failed": 1, "duplicate": 1 },
        "created_at": 1790780400000,
        "completed_at": 1790784000000
    },
    "timestamp": 1790784060000
}
```

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

### List Batch Items

```http theme={null}
GET /payments/register/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`, `registered`, `failed` or `duplicate`. Anything else is `400 invalid_status` |

```bash theme={null}
curl "https://api.gigstack.io/v2/payments/register/batch/pbatch_7f3a9c1e5b2d4f6a8c0e2a4b6d8f0a1c/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": "txn-2026-10-000123",
                "status": "registered",
                "payment_id": "payment_a1b2c3d4e5",
                "error": null,
                "attempts": 1
            },
            {
                "index": 2,
                "idempotency_key": "txn-2026-10-000125",
                "status": "failed",
                "payment_id": null,
                "error": { "code": "validation_failed", "message": "Resource resolution failed" },
                "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` items. Its `code` is what `POST /payments/register` would have answered for the same body (`invalid_request_body` for a date in the future or a missing exchange rate, `validation_failed` for a client or service that doesn't resolve, …). 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, including a rotated key whose overlap ended. The remaining items fail the same way | Send the failed items in a new batch with a valid key |
| `credential_expired` | The API key that created the batch reached its expiry before this item ran. The remaining items fail the same way | Send the failed items in a new batch with a valid key |
| `credential_scope_changed` | The API key that created the batch no longer covers its team, account, mode or action. The remaining items fail the same way | Send the failed items in a new batch with a key that covers them |
| `http_<status>` | The payment endpoint answered that status without a code | Read `message`, fix, and send again |
| `auth_unavailable`, `internal_error`, `resource_conflict` | 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.

**Finding the payments.** `payment_id` reads the payment with [`GET /payments/{id}`](https://docs.gigstack.io/guides/payments#get-payment); the invoice an item's automation created is on the payment's `invoices`. You can also find a payment by your own key with [`GET /payments/search`](https://docs.gigstack.io/guides/payments#search-payments).

## Following Progress

**Polling.** Poll `GET /payments/register/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](https://docs.gigstack.io/guides/webhooks.md) to `payment_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_3c8d1e5f7a9b2c4d",
    "event": "payment_batch.completed",
    "created_at": 1790784000,
    "data": {
        "id": "pbatch_7f3a9c1e5b2d4f6a8c0e2a4b6d8f0a1c",
        "livemode": true,
        "total": 120,
        "accepted": 119,
        "rejected": 1,
        "counts": { "queued": 0, "registered": 117, "failed": 1, "duplicate": 1 },
        "result": "partially_completed"
    }
}
```

The delivery is signed with the webhook's `secret` (see [Verifying Signatures](https://docs.gigstack.io/guides/webhooks.md#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 payment the batch registers also sends the usual `payment.created` and `payment.succeeded` events, like any other registered payment, and an invoice created by its automation sends `invoice.created`.

## Response Objects

### Batch

| Field | Type | Description |
| - | - | - |
| `id` | string | `pbatch_` + 32 hex characters. Derived from team, mode and `Idempotency-Key` |
| `object` | string | Always `payment_batch` |
| `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 | Payments in the request |
| `accepted` | integer | Payments 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`, `registered`, `failed`, `duplicate`. 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 `payments`, from 0 |
| `idempotency_key` | string | The item's key, trimmed |
| `status` | string | See [Item Status](#item-status) |
| `payment_id` | string \| null | For `GET /payments/{id}`. Set for `registered` and `duplicate` |
| `error` | object \| null | `{ code, message }` for `failed`. See [Item errors](#item-errors) |
| `attempts` | integer | Registration attempts so far |

## Error Handling

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

| Status | `error.code` | Endpoints | When |
| - | - | - | - |
| `400` | `invalid_request_body` | `POST /payments/register/batch` | `Idempotency-Key` missing or malformed |
| `400` | `invalid_body` | `POST /payments/register/batch` | The body is not `{ "payments": [ … ] }` with at least one item |
| `400` | `too_many_items` | `POST /payments/register/batch` | More than 10,000 items |
| `400` | `invalid_limit`, `invalid_cursor`, `invalid_status` | `GET …/items` | A query parameter is out of range or unknown |
| `403` | `forbidden` | `POST /payments/register/batch` | A user-scoped token without `editor` permission on payments |
| `404` | `not_found` | `GET` endpoints | No payment batch with this id in your team and mode |
| `409` | `idempotency_key_reused` | `POST /payments/register/batch` | The `Idempotency-Key` was used before with a different body |

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](https://docs.gigstack.io/guides/welcome.md#authentication-errors).

## Related Resources

* [Payments API](https://docs.gigstack.io/guides/payments.md) - the single `POST /payments/register`, and reading payments
* [Invoice Batches API](https://docs.gigstack.io/guides/invoice-batches.md) - the same mechanism for income invoices
* [Webhooks API](https://docs.gigstack.io/guides/webhooks.md) - `payment_batch.completed`


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