---
title: "Bulk operations"
method: POST
path: "/v3/{ledgerName}/bulk"
tags: ["Transactions"]
---

# Bulk operations

`POST /v3/{ledgerName}/bulk`

## Request

- Base URL: `http://localhost:9000`
- URL: `http://localhost:9000/v3/{ledgerName}/bulk`
- Auth: one of:
  - HTTP bearer
  - none

Execute multiple operations (create transactions, add metadata,
revert transactions, delete metadata) in a single request. Requires
`ledger:write` scope.

With `atomic=true`, all elements target the URL's ledger and are
submitted as one batch. `Idempotency-Key` identifies that whole batch;
per-element `ik` values are ignored. With `atomic=false`
(default), each element is submitted separately under its own
`ik`; the header is ignored.

`continueOnFailure` controls sequential continuation and business-error
status rollup. It does not allow partial application of an atomic
batch. Non-atomic failures can follow already committed elements:
inspect every result and preserve the original identities when retrying.
A timeout or infrastructure failure does not prove that work was not
committed. This contract does not enable automatic SDK retries.

The response status is decided in three tiers:

1. **Request-level failures** (before per-element processing):
   malformed body → `400`, missing/insufficient scope → `401` or
   `403`, too many elements → `413`. Exceeding the byte limit while
   decoding the body currently returns `400`.
   Returned unconditionally, `continueOnFailure` does not apply.
2. **Infrastructure failures during processing** (transport
   timeout, cache-horizon exceeded, storage error): the overall
   status is `503` for retryable failures and `500` otherwise,
   regardless of `continueOnFailure`. Callers
   must inspect the element outcomes before deciding what to retry.
3. **Per-element errors during processing**: each element's error
   carries the HTTP status it would surface as a single request
   (via the same mapping `handleError` uses). The top-level
   status is the "worst" of them, with `continueOnFailure`
   applying only to per-element business failures (4xx):
   - Business failures (validation, not-found, conflict,
     precondition, permission — 4xx): rolled up per
     `continueOnFailure`. `true` → `200`; `false` (default) →
     the highest 4xx among failing elements (typically `400`).
   - Resource exhaustion (`KindResourceExhausted`, `429`), including
     rate limits and permanent authoritative-sequence exhaustion:
     surface unconditionally as `429`.
   - Retryable infrastructure failures (leader loss,
     cache-horizon exceeded, `KindUnavailable`): surface as
     `503` with `Retry-After: 1`, regardless of
     `continueOnFailure`.
   - Non-retryable infrastructure failures (transport / storage
     errors, `KindInternal`, unclassified): surface as `500`.
   - Aborted elements (`context.Canceled` from the sequential
     runner after a prior failure): contribute nothing — the
     originating error drives the rollup.

## Path parameters

- `ledgerName` string, required

## Query parameters

- `continueOnFailure` boolean
- `atomic` boolean

## Headers

- `Formance-Bigint-As-String` string
- `Idempotency-Key` string

## Request body

- BulkElement[]
  - `action` 'CREATE_TRANSACTION' | 'ADD_METADATA' | 'REVERT_TRANSACTION' | 'DELETE_METADATA', required: The type of operation to perform. The REST bulk endpoint accepts these four actions only; account-type actions (`ADD_ACCOUNT_TYPE` / `REMOVE_ACCOUNT_TYPE`) are gRPC-only and are rejected here.
  - `ik` string: Idempotency key for this operation
  - `data` union, required: Operation-specific data
    - CreateTransactionRequest: A transaction must declare exactly one content source: explicit `postings`, an inline `script`, or a `scriptReference`. The `postings` array cannot be combined with `script` or `scriptReference`. Idempotency is controlled exclusively via the `Idempotency-Key` HTTP header — there is no body-level key.
      - `accountMetadata` object: Metadata for accounts involved in the transaction (supports typed values). Account addresses must be nonempty and contain only letters, digits, colons, underscores and hyphens, without empty segments. Malformed addresses are rejected before submission; competing invalid addresses select the lexicographically smallest address. Each account's map is bounded independently. Cluster policy defaults allow 128 entries per entity, 256 bytes per key, 16384 bytes per value, 65536 bytes per entity, and 1048576 bytes per request. Effective ceilings are configurable through the replicated cluster policy.
      - `timestamp` string, date-time: Transaction timestamp (ISO 8601 format). If not provided, current time is used.
      - `metadata` object: Transaction metadata (supports typed values). Cluster policy defaults allow 128 entries per entity, 256 bytes per key, 16384 bytes per value, 65536 bytes per entity, and 1048576 bytes per request. Effective ceilings are configurable through the replicated cluster policy.
      - `reference` string: Transaction reference
      - `postings` PostingRequest[]: List of postings for the transaction. Mutually exclusive with `script` and `scriptReference`.
        - `source` string, required: Source account address
        - `destination` string, required: Destination account address
        - `amount` union, required: Unsigned amount from 0 through 2^256-1. Requests accept JSON integer tokens or canonical decimal strings independently of the response header; the server enforces the bound for both forms. Responses use numbers by default and strings with Formance-Bigint-As-String enabled, as do all response Uint256 values regardless of business meaning.
          - integer
          - string
        - `asset` string, required: Asset identifier
        - `color` string: Optional segregation key for "color of money". The empty string (or omitted field) means the uncolored bucket. Two postings on the same (account, asset) but different colors operate on strictly isolated balances. Color is constrained to `^[A-Z]*$`, capped at 32 characters, and is immutable once carried by funds.
      - `script` Script
        - `plain` string, required: The numscript source code
        - `vars` object: Variables to pass to the script
      - `scriptReference` ScriptReference
        - `name` string, required: Name of the numscript in the ledger's library
        - `version` string, required: Version selector: the literal `latest` or an exact full semver (`1.2.3`). Partial selectors such as `1` or `1.2` are read-only and rejected here.
        - `vars` object: Variables to pass to the referenced script
      - `force` boolean: Skip the balance check, so the transaction commits even when a source account has insufficient funds.
    - AddMetadataRequest
      - `targetType` 'ACCOUNT' | 'TRANSACTION', required: Type of target (account or transaction)
      - `targetId` union, required: ID of the target (account address or transaction ID). Required.
        - string
        - integer
      - `metadata` object, required: Metadata to add (supports typed values). Cluster policy defaults allow 128 entries per entity, 256 bytes per key, 16384 bytes per value, 65536 bytes per entity, and 1048576 bytes per request. Effective ceilings are configurable through the replicated cluster policy.
    - RevertTransactionRequest
      - `id` integer, required: Transaction ID to revert. Required.
      - `force` boolean: Force revert even if accounts don't have sufficient funds
      - `atEffectiveDate` boolean: Set the timestamp of the reverted transaction to the original transaction timestamp
      - `metadata` object: Metadata for the revert transaction (supports typed values). Cluster policy defaults allow 128 entries per entity, 256 bytes per key, 16384 bytes per value, 65536 bytes per entity, and 1048576 bytes per request. Effective ceilings are configurable through the replicated cluster policy.
    - DeleteMetadataRequest
      - `targetType` 'ACCOUNT' | 'TRANSACTION', required: Type of target (account or transaction)
      - `targetId` union, required: ID of the target (account address or transaction ID). Required.
        - string
        - integer
      - `key` string, required: Metadata key to delete
  - `skippableReasons` string[]: Opt-in continue-on-failure list for this entry. When the FSM would otherwise reject the underlying order with a business error whose typed reason matches one of these entries, the order is recorded as an `OrderSkippedLog` (no state mutation) and the bulk result's `data` surfaces the skip context. Reasons outside the per-action business whitelist (structural / internal reasons) are rejected at admission. Accepted reasons per action on this REST endpoint: - `CREATE_TRANSACTION` → `TRANSACTION_REFERENCE_CONFLICT` - `REVERT_TRANSACTION` → `TRANSACTION_ALREADY_REVERTED` - `DELETE_METADATA` → `METADATA_NOT_FOUND` The FSM also supports skipping `ADD_ACCOUNT_TYPE` (`ACCOUNT_TYPE_ALREADY_EXISTS`) and `REMOVE_ACCOUNT_TYPE` (`ACCOUNT_TYPE_NOT_FOUND`), but those actions are **gRPC-only**: the REST bulk endpoint does not accept account-type actions (see the `action` enum above), so their reasons are not offered here. Empty (default) preserves the historical fail-on-error behaviour.

## Response `200`

Bulk operations completed

- BulkResponse
  - `data` BulkResult[]
    - `responseType` string: Type of response (action name or ERROR)
    - `logID` integer: Log ID of the applied operation (0 on error)
    - `errorCode` string: Error code (present only on error)
    - `errorDescription` string: Client-facing error description (present only on error). Unknown internal failures carry a generic message with a correlation ID; internal index/coverage details are omitted. Machine-readable error codes and the bulk response shape are preserved.
    - `data` union: Operation result data. Shape depends on the originating action: - `CREATE_TRANSACTION` success: a `TransactionResponse` — the created Transaction object. - Any skippable action skip (when the item opted into `skippableReasons` and the FSM matched one): an `OrderSkippedResponse` with `skipped: true`, `reason`, and reason-specific `context`. Emitted for every REST-supported skippable action — CREATE_TRANSACTION, REVERT_TRANSACTION, DELETE_METADATA — not just create. - Other successful non-create actions: `null`. Branch on `OrderSkippedResponse.skipped` to distinguish a skip from a successful transaction.
      - TransactionResponse
        - `postings` PostingResponse[], required
          - `source` string, required
          - `destination` string, required
          - `amount` union, required: Unsigned amount from 0 through 2^256-1. Requests accept JSON integer tokens or canonical decimal strings independently of the response header; the server enforces the bound for both forms. Responses use numbers by default and strings with Formance-Bigint-As-String enabled, as do all response Uint256 values regardless of business meaning.
            - integer
            - string
          - `asset` string, required
          - `color` string, required: Segregation key for "color of money". The empty string means the uncolored bucket. Always present on responses (never omitted); two postings on the same (account, asset) but different colors operate on strictly isolated balances.
        - `metadata` object, required
        - `timestamp` string, date-time: Transaction timestamp (ISO 8601 format)
        - `reference` string
        - `id` integer, required: Transaction ID (assigned by the system)
        - `reverted` boolean, required: Whether this transaction has been reverted
        - `insertedAt` string, date-time: When the transaction was inserted into the ledger
        - `updatedAt` string, date-time: Last update timestamp
        - `revertedAt` string, date-time, nullable: When the transaction was reverted (null if not reverted)
        - `revertedByTransactionId` integer: ID of the transaction that reverted this one (absent when not reverted)
        - `revertsTransactionId` integer: ID of the original transaction this transaction reverts (absent when this is not a revert)
        - `postCommitVolumes` PostCommitVolumes: Post-commit volumes, keyed by account address. Each account maps to a flat array of per-(asset, color) volume tuples. The wire is a plain `{"addr": [{asset, color, input, output}]}` map — the proto `volumesByAccount` / `volumes` wrappers are flattened away by `PostCommitVolumes.MarshalJSON`. This replaces the pre-color `{"addr": {"asset": Volumes}}` map shape, which can no longer key a bucket uniquely once a color dimension exists.
      - OrderSkippedResponse: Shape returned in the bulk result's `data` field when a bulk entry opted into `skippableReasons` and the FSM matched one of the listed reasons (no state mutation occurred). `skipped` is always `true` on this shape — branch on it to distinguish from the normal Transaction data.
        - `skipped` boolean, required: Always true on this shape.
        - `reason` string, required: Typed reason that matched the skip (e.g. `TRANSACTION_REFERENCE_CONFLICT`). Same identifier the gRPC ErrorInfo.reason and the REST error responses use.
        - `code` string, required: Stable subcode for the skipped failure.
        - `facts` object: Validated, reason-specific correlators. For `TRANSACTION_REFERENCE_CONFLICT`, contains `reference` and `existingTransactionId`.
  - `errorCode` string
  - `errorMessage` string

## Other responses

- `400`: Bulk operations completed with errors, or invalid request
- `401`: Authentication failure before processing or in an element
- `403`: Insufficient scope before processing or permission failure in an element
- `404`: A ledger or resource was not found during processing
- `405`: Method not allowed
- `409`: An element or the atomic batch conflicts with existing state
- `413`: Request entity too large
- `429`: A per-element error mapped to `KindResourceExhausted` (quota, rate limit, or authoritative-sequence exhaustion during processing). Surfaces unconditionally, regardless of `continueOnFailure`. Body carries the per-element details in `data`.
- `500`: Processing failure with per-element outcomes, or an ordinary error from panic recovery. Surfaces regardless of continueOnFailure. An infrastructure failure does not establish whether work committed.
- `503`: Retryable processing failure, including leader loss or an expired admission cache horizon. Surfaces regardless of continueOnFailure. Inspect per-element outcomes before retrying a non-atomic batch.

## Changes

- **2026-11-03** `f5e817eca038`: 1 info
  - api operation id `bulkOperations` removed and replaced with `v3BulkOperations`
- **2026-10-14** `5e39ed0a6af9`: 9 warning, 18 info
  - removed the optional property `anyOf[#/components/schemas/BulkResponse]/data/items/data/oneOf[#/components/schemas/OrderSkippedResponse]/context` from the response with the `400` status
  - removed the optional property `anyOf[#/components/schemas/BulkResponse]/data/items/data/oneOf[#/components/schemas/OrderSkippedResponse]/context` from the response with the `401` status
  - removed the optional property `anyOf[#/components/schemas/BulkResponse]/data/items/data/oneOf[#/components/schemas/OrderSkippedResponse]/context` from the response with the `403` status
  - removed the optional property `anyOf[#/components/schemas/BulkResponse]/data/items/data/oneOf[#/components/schemas/OrderSkippedResponse]/context` from the response with the `404` status
  - …23 more
- …earlier changes not shown

[Operation history](https://skmtc.dev/formancehq/apis/ledger-api/changes/v3/:ledgerName/bulk/post.md)

---

[API](https://skmtc.dev/formancehq/apis/ledger-api.md) - [All operations](https://skmtc.dev/formancehq/apis/ledger-api/llms.txt) - [OpenAPI document](https://skmtc.dev/formancehq/apis/ledger-api/revisions/f5e817eca038?raw)
