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

# Execute a batch of expense API requests

> Batches multiple expense API calls into a single HTTP request. Each sub-request in `requests` is dispatched internally to its normal controller/action and is authenticated and authorized against its own target endpoint, exactly as if it had been called directly — a caller can never reach an endpoint through a batch that they could not reach on their own.

**Scope lock:** every sub-request in one batch must resolve to the same endpoint (same HTTP method and the same target action, resolved through the real URL mappings). For example, approving expense reports A, B and C in one call is allowed (`PUT /me/organizations/{id}/approvals/{expenseReportId}` repeated for each), but mixing an approval request with a request to update an expense in the same batch is rejected with `400`.

Sub-request paths must start with `/v1/expense/` (the `/api` context prefix is optional and is stripped if present). Nesting a bulk call inside another bulk call is rejected.

Sub-requests run sequentially and each commits independently in its own database session — there is no all-or-nothing behavior across the batch, so a failing sub-request does not roll back or block the others (partial success). The overall response is `200` whenever the batch envelope itself is well-formed, even if individual sub-requests failed; check each entry's own `status` for its outcome.

Binary or streamed endpoints (images, PDFs, content downloads) are not supported inside a batch and return a per-entry `415` — call them directly instead.



## OpenAPI

````yaml /openapi/findity-expense-api.json post /bulk
openapi: 3.0.3
info:
  title: Findity Expense API
  description: Findity Expense API
  version: 1.0.0
servers:
  - url: https://stage-api.findity.com/api/v1/expense
  - url: https://api.findity.com/api/v1/expense
security:
  - bearerAuth: []
paths:
  /bulk:
    post:
      tags:
        - Bulk
      summary: Execute a batch of expense API requests
      description: >-
        Batches multiple expense API calls into a single HTTP request. Each
        sub-request in `requests` is dispatched internally to its normal
        controller/action and is authenticated and authorized against its own
        target endpoint, exactly as if it had been called directly — a caller
        can never reach an endpoint through a batch that they could not reach on
        their own.


        **Scope lock:** every sub-request in one batch must resolve to the same
        endpoint (same HTTP method and the same target action, resolved through
        the real URL mappings). For example, approving expense reports A, B and
        C in one call is allowed (`PUT
        /me/organizations/{id}/approvals/{expenseReportId}` repeated for each),
        but mixing an approval request with a request to update an expense in
        the same batch is rejected with `400`.


        Sub-request paths must start with `/v1/expense/` (the `/api` context
        prefix is optional and is stripped if present). Nesting a bulk call
        inside another bulk call is rejected.


        Sub-requests run sequentially and each commits independently in its own
        database session — there is no all-or-nothing behavior across the batch,
        so a failing sub-request does not roll back or block the others (partial
        success). The overall response is `200` whenever the batch envelope
        itself is well-formed, even if individual sub-requests failed; check
        each entry's own `status` for its outcome.


        Binary or streamed endpoints (images, PDFs, content downloads) are not
        supported inside a batch and return a per-entry `415` — call them
        directly instead.
      operationId: expenseBulk
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkRequest'
            example:
              requests:
                - id: '0'
                  method: PUT
                  path: >-
                    /v1/expense/me/organizations/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/approvals/f328e47d595c40ad80f27f52626c5b92
                  body:
                    approvalChainId: 3414bbc71ae8401aafa278cc1fac221b
                - id: '1'
                  method: PUT
                  path: >-
                    /v1/expense/me/organizations/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/approvals/b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5
                  body:
                    approvalChainId: 3414bbc71ae8401aafa278cc1fac221b
      responses:
        '200':
          description: >-
            The batch envelope was well-formed. Individual sub-requests may
            still have failed or been unauthorized — check each entry's own
            `status`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkResponse'
              example:
                responses:
                  - id: '0'
                    index: 0
                    status: 200
                    headers:
                      Content-Type: application/json
                    body:
                      id: f328e47d595c40ad80f27f52626c5b92
                      status: APPROVED
                  - id: '1'
                    index: 1
                    status: 403
                    body:
                      result: error
                      description: Not allowed to approve this report
        '400':
          description: >-
            The batch envelope itself is malformed — invalid JSON, a missing
            `requests` array, more than 50 sub-requests, or a sub-request that
            fails path/method/scope validation. The whole batch is rejected and
            no sub-requests are dispatched.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/400Error'
              example:
                result: error
                code: INVALID_PARAMETER
                description: 'Bulk request at index 1: path must start with ''/v1/expense/'''
components:
  schemas:
    BulkRequest:
      type: object
      required:
        - requests
      properties:
        requests:
          type: array
          description: >-
            Sub-requests to dispatch, in order. Sub-requests run sequentially
            and each commits independently — there is no all-or-nothing behavior
            across the batch, so a failing sub-request does not roll back or
            block the others.
          minItems: 1
          maxItems: 50
          items:
            $ref: '#/components/schemas/BulkSubRequest'
    BulkResponse:
      type: object
      properties:
        responses:
          type: array
          description: >-
            One entry per sub-request in the original `requests` array, in the
            same order.
          items:
            $ref: '#/components/schemas/BulkResponseEntry'
    400Error:
      type: object
      properties:
        result:
          type: string
        code:
          type: string
        description:
          type: string
    BulkSubRequest:
      type: object
      required:
        - method
        - path
      properties:
        id:
          type: string
          description: >-
            Optional client-supplied identifier for this sub-request. Echoed
            back verbatim on the matching entry in the response's `responses`
            array so callers can correlate results without relying on array
            order. Not required — every response entry also carries the
            sub-request's zero-based `index`.
          maxLength: 100
          example: '0'
        method:
          type: string
          description: HTTP method to dispatch this sub-request with.
          enum:
            - GET
            - POST
            - PUT
            - DELETE
        path:
          type: string
          description: >-
            API path to dispatch to. Must start with the same prefix as the bulk
            endpoint being called (`/v1/admin/` for the admin bulk endpoint,
            `/v1/expense/` for the expense bulk endpoint). The leading `/api`
            context prefix is optional and is stripped if present.
            Percent-encoding, path traversal (`..`), query strings (`?`), and
            nested `/bulk` paths are rejected. All sub-requests within one batch
            must resolve to the same underlying endpoint (same HTTP method and
            the same target action) — see the endpoint description for the
            scope-lock rule.
          example: /v1/expense/expenses/f328e47d595c40ad80f27f52626c5b92
        params:
          type: object
          description: >-
            Query/form parameters to send with the sub-request, as key-value
            pairs. Each value must be a string, number, or boolean, or an array
            of those — nested objects are rejected. Authentication-bearing
            parameters (e.g. `authenticationToken`) are silently stripped; the
            sub-request always runs as the caller who made the outer bulk call.
          additionalProperties: true
        body:
          type: object
          description: >-
            JSON body to send with the sub-request, matching the shape the
            target endpoint normally expects. Ignored when `method` is `GET`.
            Limited to 1 MB per sub-request.
    BulkResponseEntry:
      type: object
      properties:
        id:
          type: string
          description: >-
            Echoes the `id` supplied on the matching sub-request, or its
            zero-based array index (as a string) when no `id` was supplied.
        index:
          type: integer
          description: >-
            Zero-based position of this entry's sub-request in the original
            `requests` array.
        status:
          type: integer
          description: >-
            HTTP status code returned by the target endpoint for this
            sub-request (for example `200`, `401`, `404`). `415` means the
            target endpoint returns binary/streamed content, which is not
            supported inside a batch — call it directly instead. `500` means the
            sub-request failed to dispatch.
        headers:
          type: object
          description: >-
            Subset of the response headers returned by the target endpoint for
            this sub-request — currently `Content-Type` and `Location`, when
            present. Omitted entirely when neither header is set.
          additionalProperties:
            type: string
        body:
          description: >-
            JSON body returned by the target endpoint for this sub-request,
            unchanged. `null` when the target endpoint returned no body. A
            response larger than the capture limit (10 MB by default) is
            replaced with `{"omitted": true, "reason": "response too large",
            "contentType": "...", "contentLength": 0}`.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer

````