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

# Bulk requests

Run many Admin API calls to the same endpoint in a single HTTP request.

This guide explains the bulk endpoint, `POST /bulk`. You send one request that holds a list of ordinary Admin API calls; Findity runs them one after another and returns one result per call. It's the way to create or read many items — dimension values, approvers, organizations — without making one HTTP round trip each.

## Overview

A bulk request wraps up to 50 calls in one envelope. Each call (a **sub-request**) is dispatched to its normal endpoint and behaves exactly as if you had called it directly: it is authenticated and authorized against its own target, and it returns its own status code and body.

Keep these rules in mind — they shape how you design a batch:

* **One endpoint per batch.** Every sub-request in a batch must call the same endpoint (the same HTTP method and the same action). You can repeat it with different ids, but you can't mix endpoints.
* **Partial success.** Sub-requests run in order, and each one commits on its own. A failing sub-request doesn't roll back or block the others.
* **A `200` doesn't mean everything worked.** The overall response is `200` whenever the batch itself is well-formed. Always check each entry's own `status`.

## Prerequisites

1. Complete [Authentication](https://developer.findity.com/docs/accessing.md) and obtain an admin-scoped Bearer token. Every sub-request runs as the caller who made the bulk request, so the token must be allowed to call each target endpoint.
2. Know the endpoint you want to batch and its requirements — for example, to create dimension values you need the organization id and the dimension `id`.

## Key concepts

| Concept | Description |
| - | - |
| **Batch** | One `POST /bulk` request. Holds a `requests` array of 1 to 50 sub-requests. |
| **Sub-request** | One call inside the batch: an HTTP `method`, a `path`, and optionally `params` and a `body`. |
| **Same-endpoint rule** | All sub-requests in a batch must resolve to the same endpoint — the same HTTP method and the same target action. |
| **Partial success** | Each sub-request commits independently. There is no all-or-nothing behavior across the batch. |
| **Response entry** | The result of one sub-request: its `index`, your `id` (if you sent one), the target endpoint's `status`, and its `body`. |

## API endpoints

All bulk requests use the v1 admin host. Examples in this guide use the stage environment — switch to the production host only when you're ready to go live.

| Environment | Base URL |
| - | - |
| Stage | `https://stage-api.findity.com/api/v1/admin` |
| Production | `https://api.findity.com/api/v1/admin` |

| Operation | Method | Endpoint |
| - | - | - |
| Execute a batch of admin API requests | `POST` | `/bulk` |

### Required headers

Include these on the bulk request:

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `Authorization` | `Bearer {access_token}` |

## Request format

`POST /bulk`

The body is a JSON object with one required field, `requests`.

| Field | Type | Required | Description |
| - | - | - | - |
| `requests` | array | Yes | The sub-requests to run, in order. Between 1 and 50 entries. |

### Sub-request fields

| Field | Type | Required | Description |
| - | - | - | - |
| `method` | string | Yes | HTTP method for the sub-request: `GET`, `POST`, `PUT`, or `DELETE`. |
| `path` | string | Yes | The endpoint path. Must start with `/v1/admin/` (the `/api` prefix is optional and is removed if present). |
| `id` | string | Optional | Your own identifier for the sub-request, up to 100 characters. It is returned on the matching response entry so you can match results without relying on order. |
| `params` | object | Optional | Query parameters for the sub-request, as key-value pairs. Each value is a string, number, or boolean, or an array of those. Nested objects are rejected. |
| `body` | object | Optional | The JSON body for the sub-request, in the shape the target endpoint expects. Up to 1 MB (1,048,576 bytes); a larger body rejects the whole batch. Ignored when `method` is `GET`. |

Rules for `path` and `params`:

* Put query parameters in `params`, not in the path. A path that contains `?` is rejected.
* Percent-encoding and path traversal (`..`) in the path are rejected.
* A path that points at `/bulk` is rejected — you can't nest a bulk request inside another.
* Authentication parameters (such as `authenticationToken`) are removed. A sub-request always runs as the caller of the bulk request.

### The same-endpoint rule

Every sub-request in a batch must resolve to the same endpoint, matched on the HTTP method and the target action — not on the exact path. That means you can repeat one endpoint with different ids:

| Batch | Allowed? |
| - | - |
| `GET /v1/admin/organizations/{organizationId}` repeated for organizations A, B, and C | Yes |
| `POST .../customdimensions/{definitionId}/values` repeated with different value bodies | Yes |
| `GET /v1/admin/organizations/{organizationId}` mixed with `PUT /v1/admin/organizations/{organizationId}/policies/documents/{documentId}` | No — rejected with `400` |
| `PUT .../customdimensions/{definitionId}/values/{valueId}` mixed with `DELETE` on the same path | No — the HTTP method is part of the endpoint, so this is rejected with `400` |

If you need to run different operations — for example creating some values and updating others — send one batch per operation.

## Response format

The response is a JSON object with a `responses` array. It has one entry per sub-request, in the same order as `requests`.

| Field | Type | Description |
| - | - | - |
| `id` | string | The `id` you sent on the sub-request, or its zero-based index as a string when you sent none. |
| `index` | integer | Zero-based position of the sub-request in your `requests` array. |
| `status` | integer | The HTTP status code the target endpoint returned for this sub-request, for example `200`, `201`, `404`, or `409`. |
| `headers` | object | A subset of the target's response headers — `Content-Type` and `Location`, when present. Left out when neither is set. |
| `body` | any | The JSON body the target endpoint returned, unchanged. `null` when it returned no body. |

Two special statuses:

* **`415`** — the target endpoint returns binary or streamed content (images, PDFs, downloads), which isn't supported in a batch. Call that endpoint directly instead.
* **`500`** — the sub-request failed to dispatch.

A call that returns no content (for example a `DELETE`) comes back with `status` `204`, `body` `null`, and no `headers`:

```json JSON theme={"system"}
{ "id": "del", "index": 0, "status": 204, "body": null }
```

Sub-request `id`s are not checked for uniqueness: two sub-requests with the same `id` both run and both come back with that `id`, so use the `index` to tell them apart.

A response body larger than the capture limit (10 MB by default) is replaced with `{ "omitted": true, "reason": "response too large", "contentType": "...", "contentLength": 0 }`.

## Example 1 — Create dimension values in one request

This batch creates two values on a dimension. Every sub-request is a `POST` to the same values endpoint, so the batch is allowed. The organization and the dimension already exist; the path carries their ids.

`POST /bulk`

```sh cURL theme={"system"}
curl -X POST \
  "https://stage-api.findity.com/api/v1/admin/bulk" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      {
        "id": "cc-002",
        "method": "POST",
        "path": "/v1/admin/organizations/8a5c8bee9e3a4a24019e3ae1732b008e/customdimensions/ef529f9d877d4d44a34609e53e5b8226/values",
        "body": { "id": "cc-002", "value": "Engineering", "sortOrder": 2 }
      },
      {
        "id": "cc-003",
        "method": "POST",
        "path": "/v1/admin/organizations/8a5c8bee9e3a4a24019e3ae1732b008e/customdimensions/ef529f9d877d4d44a34609e53e5b8226/values",
        "body": { "id": "cc-003", "value": "Sales", "sortOrder": 3 }
      }
    ]
  }'
```

**Example response** (status `200 OK`)

```json JSON theme={"system"}
{
  "responses": [
    {
      "id": "cc-002",
      "index": 0,
      "status": 201,
      "headers": { "Content-Type": "application/json;charset=UTF-8" },
      "body": { "id": "cc-002", "value": "Engineering", "sortOrder": 2, "startDate": null, "endDate": null }
    },
    {
      "id": "cc-003",
      "index": 1,
      "status": 201,
      "headers": { "Content-Type": "application/json;charset=UTF-8" },
      "body": { "id": "cc-003", "value": "Sales", "sortOrder": 3, "startDate": null, "endDate": null }
    }
  ]
}
```

Each entry carries the target endpoint's own `201 Created` status and the created value, exactly as a direct call would return it.

## Example 2 — Read several organizations

A `GET` sub-request takes its query parameters in `params`. This batch fetches two organizations, including their address and partner sections. The second organization doesn't exist, which shows partial success.

`POST /bulk`

```sh cURL theme={"system"}
curl -X POST \
  "https://stage-api.findity.com/api/v1/admin/bulk" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      {
        "id": "0",
        "method": "GET",
        "path": "/v1/admin/organizations/f328e47d595c40ad80f27f52626c5b92",
        "params": { "include": "address,partner" }
      },
      {
        "id": "1",
        "method": "GET",
        "path": "/v1/admin/organizations/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
        "params": { "include": "address,partner" }
      }
    ]
  }'
```

**Example response** (status `200 OK`)

```json JSON theme={"system"}
{
  "responses": [
    {
      "id": "0",
      "index": 0,
      "status": 200,
      "headers": { "Content-Type": "application/json;charset=UTF-8" },
      "body": {
        "id": "f328e47d595c40ad80f27f52626c5b92",
        "name": "Acme AB"
      }
    },
    {
      "id": "1",
      "index": 1,
      "status": 404,
      "body": {
        "result": "error",
        "code": "ORGANIZATION_NOT_FOUND",
        "description": "Organization not found"
      }
    }
  ]
}
```

The overall response is `200` even though the second sub-request returned `404`. The first entry's `body` is abbreviated here.

## Example 3 — Add approvers to dimension values

This batch builds a two-step approval chain on one dimension value: John Doe approves first (`level` 0), then Carol Park (`level` 1). Both sub-requests call the same approvers endpoint, so the batch is allowed. The approval section and the two users already exist; `sectionItemId` is the `id` of a dimension value.

`POST /bulk`

```sh cURL theme={"system"}
curl -X POST \
  "https://stage-api.findity.com/api/v1/admin/bulk" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      {
        "id": "john",
        "method": "POST",
        "path": "/v1/admin/organizations/8a5c8bee9e3a4a24019e3ae1732b008e/approval/sections/abcdd5f401cf43549a688045b5ceef9b/approvers",
        "body": { "sectionItemId": "cc-002", "level": 0, "personExternalId": "person-john-doe" }
      },
      {
        "id": "carol",
        "method": "POST",
        "path": "/v1/admin/organizations/8a5c8bee9e3a4a24019e3ae1732b008e/approval/sections/abcdd5f401cf43549a688045b5ceef9b/approvers",
        "body": { "sectionItemId": "cc-002", "level": 1, "personExternalId": "person-carol-park" }
      }
    ]
  }'
```

**Example response** (status `200 OK`)

```json JSON theme={"system"}
{
  "responses": [
    {
      "id": "john",
      "index": 0,
      "status": 201,
      "headers": { "Content-Type": "application/json;charset=UTF-8" },
      "body": {
        "id": "8ab28c4f6f1da520016f23777f0e01b1",
        "personId": "8ab28c4f6f1da520016f2377703c01a7",
        "personExternalId": "person-john-doe",
        "firstName": "John",
        "lastName": "Doe",
        "email": "john.doe@acme.com",
        "level": 0,
        "amountLimit": null
      }
    },
    {
      "id": "carol",
      "index": 1,
      "status": 201,
      "headers": { "Content-Type": "application/json;charset=UTF-8" },
      "body": {
        "id": "8ab28c4f6f1da520016f23777f0e01b2",
        "personId": "f21e3a195d2d4744945ac73c0da95fc7",
        "personExternalId": "person-carol-park",
        "firstName": "Carol",
        "lastName": "Park",
        "email": "carol.park@acme.com",
        "level": 1,
        "amountLimit": null
      }
    }
  ]
}
```

## Handling partial failure

Because each sub-request commits on its own, a batch can succeed in part. Check the result of every entry, not just the overall `200`:

1. Loop over `responses` and read each entry's `status`.
2. Match failures back to your requests with the `id` you supplied (or the `index`).
3. Retry or fix only the entries that failed. Don't resend the whole batch — the sub-requests that succeeded have already been applied, and sending them again can fail (for example, creating a dimension value whose `id` already exists returns `409`).

A failed entry carries the target endpoint's own error in its `body`. For example, a duplicate dimension value returns:

```json JSON theme={"system"}
{
  "id": "cc-002",
  "index": 0,
  "status": 409,
  "body": {
    "result": "error",
    "requestId": "0b7f3c9e-4d2a-4a1e-9c55-6f1d2e8a7b30",
    "description": "A dimension value with id 'cc-002' already exists"
  }
}
```

## Common integration patterns

**Batches larger than 50.** A batch holds at most 50 sub-requests. Split a longer list into chunks of 50 and send one bulk request per chunk.

**One batch per operation.** Because a batch can call only one endpoint, a sync that creates, updates, and deletes items needs one batch per operation — for example, one batch of `POST` calls for new dimension values, one of `PUT` calls for changed values, and one of `DELETE` calls for removed values.

**Reading many items.** Use `GET` sub-requests to fetch several organizations, or several records of the same kind, in one round trip. Send query parameters such as `include` in `params`.

**Several organizations.** The same-endpoint rule is about the endpoint, not the path ids, so one batch can call the same endpoint for several organizations by putting each organization's id in its sub-request path.

## Error handling

A bulk request can fail in two places: the batch as a whole, or an individual sub-request.

**The batch is rejected.** If the batch itself is malformed, the response is `400` and no sub-request is run:

```json JSON theme={"system"}
{
  "result": "error",
  "requestId": "32525ac4-0eed-449f-91ce-a1f37c62a394",
  "code": "INVALID_PARAMETER",
  "description": "Bulk 'requests' array must not be empty"
}
```

Every case below returns `400` and runs nothing. Match on the `code`: the `description` is human-readable text that can be translated (stage returned a Swedish message for invalid JSON), and where a sub-request is at fault it names its position (`index`).

| Cause | `code` | Example `description` |
| - | - | - |
| The body isn't valid JSON | `INVALID_JSON_BODY` | Failed to parse the JSON body (the text may be translated) |
| `requests` is missing or not an array | `INVALID_PARAMETER` | `Bulk request must contain a 'requests' array` |
| `requests` is empty | `INVALID_PARAMETER` | `Bulk 'requests' array must not be empty` |
| More than 50 sub-requests | `INVALID_PARAMETER` | `Bulk request exceeds the maximum of 50 sub-requests` |
| The sub-requests don't all call the same endpoint | `INVALID_PARAMETER` | `All sub-requests in a bulk must target the same endpoint; found [GET adminApiAccountingNumber:create, GET adminApiOrganization:null]` |
| A path doesn't start with `/v1/admin/` | `INVALID_PARAMETER` | `Bulk request at index 0: path must start with '/v1/admin/'` |
| A path has a query string, percent-encoding, or `..` | `INVALID_PARAMETER` | `Bulk request at index 0: invalid path '…'` |
| A sub-request calls `/bulk` | `INVALID_PARAMETER` | `Bulk request at index 0: nested bulk requests are not allowed` |
| A method other than `GET`, `POST`, `PUT`, `DELETE` | `INVALID_PARAMETER` | `Bulk request at index 0: method 'PATCH' is not allowed (use one of [GET, POST, PUT, DELETE])` |
| A `params` value is an object | `INVALID_PARAMETER` | `Bulk request at index 0: param 'include' must be a scalar or array of scalars, not an object` |
| A sub-request `body` is larger than 1 MB | `INVALID_PARAMETER` | `Bulk request at index 0: body exceeds the maximum of 1048576 bytes` |

**A sub-request fails.** The overall response is `200`, and the failure is in that entry:

| Entry `status` | Meaning |
| - | - |
| The target endpoint's own error status (for example `400`, `403`, `404`, `409`) | The sub-request failed or wasn't authorized. The `body` holds that endpoint's normal error. The other sub-requests are unaffected. |
| `415` | The target endpoint returns binary or streamed content and can't run in a batch. Call it directly. |
| `500` | The sub-request failed to dispatch. |
