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

# Batch Operations

## Asynchronous Execution

Batch operations are asynchronous. Every batch endpoint returns an [`async_task`](/api-reference/async-tasks/retrieve-an-async-task) handle that you use to track completion and retrieve the affected entities.

The task **may or may not have completed** by the time you receive the response — `data.status` could be `processing` or `completed`. **Partners must always check `data.status` and not assume the work has finished.**

You have two ways to observe completion:

* **Poll** the URL in `links.self` on the response until `data.status` is `completed` (or `error`).
* **Subscribe** to the [`async_task_completed`](/api/overview/webhook-structure) webhook event, which fires when the task finishes, whether it succeeded or failed.

The webhook carries no status. Fetch the task to see how it ended: `data.status` is `completed` on success and `error` on failure. See [Failed Tasks](/api/bulk-and-batch-operations/bulk-and-batch-operations#failed-tasks) for the failure details a batch reports.

## Batch Upsert

Nmbr's API allows you to create or update ("upsert") multiple entities of a single type in a single API call.

### Endpoint

`POST /<type>s/batch/upsert`

`<type>` can be one of the following types:

* Recurrences
  * `pay_rate`
  * `allowance`
  * `deduction`
  * `earning`
  * `employee_benefit`
  * `employer_benefit`
  * `reimbursement`
* Line Items
  * `allowance_line_item`
  * `deduction_line_item`
  * `earning_line_item`
  * `employee_benefit_line_item`
  * `employer_benefit_line_item`
  * `reimbursement_line_item`
* Other
  * `employee`
  * `contractor`
  * `work_assignment`
  * `pay_stub`

### Request

The body of the request must be an array of entity objects. These properties of the entity objects are documented in the API reference for each entity type, but in general the batch upsert endpoint takes the same properties with the same restrictions as the one-off create or update endpoints.

If an object includes an `id` property, the existing entity with that ID will be updated. Otherwise, a new entity will be created.

For example, this request updates one existing earning line item (with ID `ernli_01J8KXC9R4MQVW2FXZN7Y5H3B8`) and creates two new earning line items:

`POST /earning_line_items/batch/upsert`

```json theme={null}
[
  {
    "id": "ernli_01J8KXC9R4MQVW2FXZN7Y5H3B8",
    "custom_amount": 500.0,
    "custom_hours": 5
  },
  {
    "pay_stub_id": "payst_01J8KXB4N6RQWM2FVZH9Y3T5C8",
    "earning_type": "wage",
    "title": "Tuesday Wages",
    "custom_amount": 300.0,
    "custom_hours": 3
  },
  {
    "pay_stub_id": "payst_01J8KXB4N6RQWM2FVZH9Y3T5C8",
    "earning_type": "wage",
    "title": "Wednesday Wages",
    "custom_amount": 400.0,
    "custom_hours": 4
  }
]
```

### Response

On success, the operation returns `202 Accepted` with an [`async_task`](/api-reference/async-tasks/retrieve-an-async-task) handle. See [Asynchronous Execution](#asynchronous-execution) for how to track completion. **⚠️ Partners must treat any 200-level response as success. ⚠️**

For example, the response to the upsert request above might look like:

```json theme={null}
{
  "id": "asnct_01KS0G8Z2YD3T9KQNFW1XEA7HB",
  "object": "async_task",
  "data": {
    "type": "batch_upsert",
    "status": "processing",
    "completed_at": null,
    "results": [],
    "created_at": "2026-05-19T16:14:32Z",
    "updated_at": "2026-05-19T16:14:32Z"
  },
  "links": {
    "self": "/async_tasks/asnct_01KS0G8Z2YD3T9KQNFW1XEA7HB"
  }
}
```

Once the task completes — either when you poll `links.self` and see `data.status` change, or when the [`async_task_completed`](/api/overview/webhook-structure) webhook arrives — `data.results` will contain one entry per request entity, in the same order. `results[n]` corresponds to `data[n]` in the request body, whether the entity was created or updated:

```json theme={null}
{
  "id": "asnct_01KS0G8Z2YD3T9KQNFW1XEA7HB",
  "object": "async_task",
  "data": {
    "type": "batch_upsert",
    "status": "completed",
    "completed_at": "2026-05-19T16:14:35Z",
    "results": [
      {
        "id": "ernli_01J8KXC9R4MQVW2FXZN7Y5H3B8",
        "object": "earning_line_item"
      },
      {
        "id": "ernli_01KBMZDVV9G7713DJRYP9RJFTP",
        "object": "earning_line_item"
      },
      {
        "id": "ernli_01KBMZE2SY5HWB4WWR9Q8960K8",
        "object": "earning_line_item"
      }
    ],
    "created_at": "2026-05-19T16:14:32Z",
    "updated_at": "2026-05-19T16:14:35Z"
  },
  "links": {
    "self": "/async_tasks/asnct_01KS0G8Z2YD3T9KQNFW1XEA7HB"
  }
}
```

On failure, the operation will return `422 Unprocessable Entity` with standard validation messages.

In order to identify which entity in the request the validation messages apply to, the validation message keys will be prefixed with `data.n.`, where `n` is the zero-based index of the entity. For example, if the first earning entity in a request had an invalid `business_preset_id` value, the validation message key would be `data.0.business_preset_id`

```
  ...
  "data.0.business_preset_id": "The selected business_preset_id is invalid.",
  ...
```

### Notes

* If any entity fails validation, no entities will be created or updated.
* When using a `business_preset_id`, properties must either be omitted or match the preset's values.
* If batch upserting line items, the payroll's totals are automatically recalculated after the batch upsert.
* If batch upserting line items, the payroll must be in draft status.

## Batch Delete Line Items

Nmbr's API allows you to delete multiple entities of a single type in a single API call.

### Endpoint

`POST /<type>s/batch/delete`

`<type>` can be one of the following types:

* Recurrences
  * `pay_rate`
  * `allowance`
  * `deduction`
  * `earning`
  * `employee_benefit`
  * `employer_benefit`
  * `reimbursement`
* Line Items
  * `allowance_line_item`
  * `deduction_line_item`
  * `earning_line_item`
  * `employee_benefit_line_item`
  * `employer_benefit_line_item`
  * `reimbursement_line_item`
* Other
  * `employee`
  * `contractor`
  * `work_assignment`
  * `pay_stub`

### Request

The body of the request must be an array of the IDs of the entities to delete.

For example, this request deletes three earning line items:

`POST /earning_line_items/batch/delete`

```json theme={null}
[
  "ernli_01J8KXC9R4MQVW2FXZN7Y5H3B8",
  "ernli_01KBMZDVV9G7713DJRYP9RJFTP",
  "ernli_01KBMZE2SY5HWB4WWR9Q8960K8"
]
```

### Response

On success, the operation returns `202 Accepted` with an [`async_task`](/api-reference/async-tasks/retrieve-an-async-task) handle. See [Asynchronous Execution](#asynchronous-execution) for how to track completion. **⚠️ Partners must treat any 200-level response as success. ⚠️**

For example, the response to the delete request above might look like:

```json theme={null}
{
  "id": "asnct_01KS0G8Z2YD3T9KQNFW1XEA7HB",
  "object": "async_task",
  "data": {
    "type": "batch_delete",
    "status": "processing",
    "completed_at": null,
    "results": [],
    "created_at": "2026-05-19T16:14:32Z",
    "updated_at": "2026-05-19T16:14:32Z"
  },
  "links": {
    "self": "/async_tasks/asnct_01KS0G8Z2YD3T9KQNFW1XEA7HB"
  }
}
```

Once the task completes — either when you poll `links.self` and see `data.status` change, or when the [`async_task_completed`](/api/overview/webhook-structure) webhook arrives — `data.results` will contain one entry per request ID, in the same order. `results[n]` corresponds to `data[n]` in the request body. Deleted entities are still surfaced by ID — the records soft-delete and remain queryable:

```json theme={null}
{
  "id": "asnct_01KS0G8Z2YD3T9KQNFW1XEA7HB",
  "object": "async_task",
  "data": {
    "type": "batch_delete",
    "status": "completed",
    "completed_at": "2026-05-19T16:14:35Z",
    "results": [
      {
        "id": "ernli_01J8KXC9R4MQVW2FXZN7Y5H3B8",
        "object": "earning_line_item"
      },
      {
        "id": "ernli_01KBMZDVV9G7713DJRYP9RJFTP",
        "object": "earning_line_item"
      },
      {
        "id": "ernli_01KBMZE2SY5HWB4WWR9Q8960K8",
        "object": "earning_line_item"
      }
    ],
    "created_at": "2026-05-19T16:14:32Z",
    "updated_at": "2026-05-19T16:14:35Z"
  },
  "links": {
    "self": "/async_tasks/asnct_01KS0G8Z2YD3T9KQNFW1XEA7HB"
  }
}
```

On failure, the operation will return `422 Unprocessable Entity` with standard validation messages.

In order to identify which entity in the request the validation messages apply to, the validation message keys will be prefixed with `data.n.`, where `n` is the zero-based index of the entity. For example, if the first entity in a request couldn't be deleted because it was a managed line item, the validation message key would be `data.0.is_managed`

```
  ...
  "data.0.is_managed": "The line item is managed and cannot be deleted.",
  ...
```

### Notes

* If any entity fails validation, no entities will be deleted.
* If batch deleting line items, the line item must not be a managed line item.
* If batch deleting line items, the payroll's totals are automatically recalculated after the batch delete.
* If batch deleting line items, the payroll must be in draft status.
