---
title: "Create a new ledger"
method: POST
path: "/v3/{ledgerName}"
tags: ["Ledgers"]
---

# Create a new ledger

`POST /v3/{ledgerName}`

## Request

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

Creates a new ledger with the specified name. Storage is configured at the server level.
Each ledger has its own Raft group for data consistency. Requires `ledger:write` scope.

The ledger name `_` is reserved: all system / non-ledger routes live
under the `/v3/_/…` segment (e.g. `/v3/_/audit-entries`), so a ledger
named `_` would be unreachable over REST. Creating it is rejected with a
validation error. The exact name `_system` is also reserved for system
events and rejected. Other underscore-prefixed names remain valid.

## Path parameters

- `ledgerName` string, required

## Headers

- `Idempotency-Key` string

## Request body

- CreateLedgerRequest
  - `metadata` object: Initial typed metadata, persisted atomically with creation in NORMAL and MIRROR modes. Creation requires ledger:WriteLedger, including initial metadata; later metadata saves require ledger:WriteMetadata. JSON null entries are omitted. Schema declarations govern indexing and queries, not write-time value coercion. 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.
  - `mode` 'NORMAL' | 'MIRROR': Ledger mode. MIRROR creates a read-only ledger that syncs from an external v2 source. Values are case-sensitive; any other value is rejected with 400 INVALID_REQUEST.
  - `mirrorSource` MirrorSourceConfig: Configuration for the mirror source (required when mode is MIRROR)
    - `ledgerName` string, required: Name of the source ledger in the v2 system
    - `type` 'http' | 'postgres': Source type (defaults to "http" if omitted)
    - `baseUrl` string: Valid absolute HTTP(S) URL with a nonempty hostname for the v2 API (required for HTTP source); checked at creation before persistence, without a network probe
    - `oauth2ClientId` string: OAuth2 client ID for the v2 API (HTTP source only)
    - `oauth2ClientSecret` string: OAuth2 client secret for the v2 API (HTTP source only)
    - `oauth2TokenEndpoint` string: OAuth2 token endpoint URL (HTTP source only)
    - `oauth2Scopes` string[]: OAuth2 scopes (HTTP source only)
    - `dsn` string: PostgreSQL connection string (required for PostgreSQL source)
    - `batchSize` integer: Max logs per batch (0 = server default 100, capped by server's --mirror-max-batch-size)
    - `rewriteRules` MirrorRewriteRule[]: Mirror rewrite rules applied, in order, to every mirror log entry as v2 logs are translated into v3 orders. Each rule is scoped to exactly one variant of the log's `data` oneof, with a CEL `match` predicate and a list of typed actions.
      - `createdTransaction` CreatedTransactionRule: Rule scoped to MirrorCreatedTransaction. Inside `match`, `log` is the created-transaction proto directly (e.g. `log.metadata["k"]`).
        - `match` string
        - `actions` CreatedTransactionAction[]
          - `rewriteAddress` RewriteAddressAction
            - `pattern` string, required: RE2 pattern (validated at admission).
            - `replacement` string, required
          - `setMetadata` SetMetadataAction: `value` is a literal metadata string; `value_expr` is a CEL expression typed against the current variant and evaluated at commit time (e.g. `log.reference`). Exactly one of the two should be set; if neither is set the value defaults to the empty string. Optional `type` coerces the produced string into a typed MetadataValue via the platform conversion matrix — one of "string" (default), "int64", "bool", "uint64", "int8"/"int16"/"int32", "uint8"/"uint16"/"uint32", "datetime".
            - `key` string, required
            - `value` string
            - `value_expr` string
            - `type` string
          - `deleteMetadata` DeleteMetadataAction
            - `key` string, required
          - `setAccountMetadata` SetAccountMetadataAction: See SetMetadataAction for the literal-vs-CEL and `type` semantics.
            - `account` string, required
            - `key` string, required
            - `value` string
            - `value_expr` string
            - `type` string
          - `deleteAccountMetadata` DeleteAccountMetadataAction
            - `account` string, required
            - `key` string, required
          - `setAccountMetadataFromAddress` SetAccountMetadataFromAddressAction: For every posting address matching `pattern`, iterate every entry in `replacements` and set accountMetadata[address][key] to the RE2 replacement of the address. Each replacement carries an optional metadata `type` token (same tokens as SetMetadataAction.type).
            - `pattern` string, required
            - `replacements` SetAccountMetadataFromAddressReplacement[], required
              - …
          - `drop` DropAction: Turns the entry into a fill-gap that preserves log-ID and tx-ID continuity.
      - `revertedTransaction` RevertedTransactionRule
        - `match` string
        - `actions` RevertedTransactionAction[]
          - `rewriteAddress` RewriteAddressAction
            - `pattern` string, required: RE2 pattern (validated at admission).
            - `replacement` string, required
          - `setMetadata` SetMetadataAction: `value` is a literal metadata string; `value_expr` is a CEL expression typed against the current variant and evaluated at commit time (e.g. `log.reference`). Exactly one of the two should be set; if neither is set the value defaults to the empty string. Optional `type` coerces the produced string into a typed MetadataValue via the platform conversion matrix — one of "string" (default), "int64", "bool", "uint64", "int8"/"int16"/"int32", "uint8"/"uint16"/"uint32", "datetime".
            - `key` string, required
            - `value` string
            - `value_expr` string
            - `type` string
          - `deleteMetadata` DeleteMetadataAction
            - `key` string, required
          - `drop` DropAction: Turns the entry into a fill-gap that preserves log-ID and tx-ID continuity.
      - `savedMetadata` SavedMetadataRule
        - `match` string
        - `actions` SavedMetadataAction[]
          - `rewriteAddress` RewriteAddressAction
            - `pattern` string, required: RE2 pattern (validated at admission).
            - `replacement` string, required
          - `setMetadata` SetMetadataAction: `value` is a literal metadata string; `value_expr` is a CEL expression typed against the current variant and evaluated at commit time (e.g. `log.reference`). Exactly one of the two should be set; if neither is set the value defaults to the empty string. Optional `type` coerces the produced string into a typed MetadataValue via the platform conversion matrix — one of "string" (default), "int64", "bool", "uint64", "int8"/"int16"/"int32", "uint8"/"uint16"/"uint32", "datetime".
            - `key` string, required
            - `value` string
            - `value_expr` string
            - `type` string
          - `deleteMetadata` DeleteMetadataAction
            - `key` string, required
          - `drop` DropAction: Turns the entry into a fill-gap that preserves log-ID and tx-ID continuity.
      - `deletedMetadata` DeletedMetadataRule
        - `match` string
        - `actions` DeletedMetadataAction[]
          - `rewriteAddress` RewriteAddressAction
            - `pattern` string, required: RE2 pattern (validated at admission).
            - `replacement` string, required
          - `drop` DropAction: Turns the entry into a fill-gap that preserves log-ID and tx-ID continuity.
      - `anyVariant` AnyVariantRule: Rule scoped to every variant. `log` is the full MirrorLogEntry — use `has(log.<variant>)` guards to read variant-specific fields.
        - `match` string
        - `actions` AnyVariantAction[]
          - `rewriteAddress` RewriteAddressAction
            - `pattern` string, required: RE2 pattern (validated at admission).
            - `replacement` string, required
          - `drop` DropAction: Turns the entry into a fill-gap that preserves log-ID and tx-ID continuity.
      - `stop` boolean
  - `defaultEnforcementMode` 'STRICT' | 'AUDIT': Default chart-of-accounts enforcement for accounts that match no account type.
  - `initialSchema` MetadataFieldTypeCommand[]: Metadata field type declarations to seed at creation time.
    - `targetType` 'account' | 'transaction' | 'ledger', required: Which entity the metadata field applies to.
    - `key` string, required: Metadata key.
    - `type` 'string' | 'int64' | 'bool' | 'uint64' | 'int8' | 'int16' | 'int32' | 'uint8' | 'uint16' | 'uint32' | 'datetime', required: Declared metadata value type.
  - `accountTypes` object: Initial account types (name -> account type definition).

## Response `201`

Ledger created successfully

- CreateLedgerResponse
  - `data` LedgerInfo, required
    - `name` string, required: Name of the ledger
    - `createdAt` string, date-time
    - `deletedAt` string, date-time: Soft deletion timestamp; omitted for active ledgers
    - `metadata` object: Enriched typed ledger metadata
    - `metadataSchema` LedgerMetadataSchema
      - `accountFields` object
      - `transactionFields` object
      - `ledgerFields` object
    - `mode` 'NORMAL' | 'MIRROR', required: Ledger mode, including the default NORMAL value
    - `mirrorSource` union: Detached credential-safe mirror configuration returned by ledger reads
      - object
        - `ledgerName` string, required
        - `batchSize` integer
        - `rewriteRules` MirrorRewriteRuleRead[]
          - `createdTransaction` CreatedTransactionRuleRead: Rule scoped to MirrorCreatedTransaction. Inside `match`, `log` is the created-transaction proto directly (e.g. `log.metadata["k"]`).
            - `match` string
            - `actions` CreatedTransactionActionRead[]
              - …
          - `revertedTransaction` RevertedTransactionRuleRead
            - `match` string
            - `actions` RevertedTransactionActionRead[]
              - …
          - `savedMetadata` SavedMetadataRuleRead
            - `match` string
            - `actions` SavedMetadataActionRead[]
              - …
          - `deletedMetadata` DeletedMetadataRuleRead
            - `match` string
            - `actions` DeletedMetadataActionRead[]
              - …
          - `anyVariant` AnyVariantRuleRead: Rule scoped to every variant. `log` is the full MirrorLogEntry — use `has(log.<variant>)` guards to read variant-specific fields.
            - `match` string
            - `actions` AnyVariantActionRead[]
              - …
          - `stop` boolean
        - `http` HttpMirrorSourceRead, required
          - `baseUrl` string: Source URL with userinfo (including passwordless tokens) and query credentials masked
          - `oauth2ClientCredentials` MirrorOAuthRead
            - `clientId` string
            - `clientSecret` '[redacted]': Presence marker only; reusable secret material is never returned
            - `tokenEndpoint` string: OAuth endpoint URL with userinfo (including passwordless tokens) and query credentials masked
            - `scopes` string[]
        - `postgres` PostgresMirrorSourceRead
          - `dsn` string: Connection string with password material masked; malformed values are fully redacted
          - `awsIamAuth` MirrorAwsIamAuthRead
            - `region` string
            - `assumeRoleArn` string
      - object
        - `ledgerName` string, required
        - `batchSize` integer
        - `rewriteRules` MirrorRewriteRuleRead[]
          - `createdTransaction` CreatedTransactionRuleRead: Rule scoped to MirrorCreatedTransaction. Inside `match`, `log` is the created-transaction proto directly (e.g. `log.metadata["k"]`).
            - `match` string
            - `actions` CreatedTransactionActionRead[]
              - …
          - `revertedTransaction` RevertedTransactionRuleRead
            - `match` string
            - `actions` RevertedTransactionActionRead[]
              - …
          - `savedMetadata` SavedMetadataRuleRead
            - `match` string
            - `actions` SavedMetadataActionRead[]
              - …
          - `deletedMetadata` DeletedMetadataRuleRead
            - `match` string
            - `actions` DeletedMetadataActionRead[]
              - …
          - `anyVariant` AnyVariantRuleRead: Rule scoped to every variant. `log` is the full MirrorLogEntry — use `has(log.<variant>)` guards to read variant-specific fields.
            - `match` string
            - `actions` AnyVariantActionRead[]
              - …
          - `stop` boolean
        - `http` HttpMirrorSourceRead
          - `baseUrl` string: Source URL with userinfo (including passwordless tokens) and query credentials masked
          - `oauth2ClientCredentials` MirrorOAuthRead
            - `clientId` string
            - `clientSecret` '[redacted]': Presence marker only; reusable secret material is never returned
            - `tokenEndpoint` string: OAuth endpoint URL with userinfo (including passwordless tokens) and query credentials masked
            - `scopes` string[]
        - `postgres` PostgresMirrorSourceRead, required
          - `dsn` string: Connection string with password material masked; malformed values are fully redacted
          - `awsIamAuth` MirrorAwsIamAuthRead
            - `region` string
            - `assumeRoleArn` string
    - `mirrorSyncProgress` MirrorSyncProgress: Sync progress for MIRROR ledgers (populated at read time)
      - `state` 'SYNCING' | 'FOLLOWING', required: Current sync state (SYNCING = catching up, FOLLOWING = up to date)
      - `cursor` string, bigint, required: Last ingested v2 log ID
      - `sourceLogCount` string, bigint, required: Latest known log ID in the v2 source
      - `remainingLogs` string, bigint, required: Number of remaining logs to sync (sourceLogCount - cursor)
      - `error` MirrorSyncError: Most recent mirror sync error (null if healthy)
        - `message` string: Error message
        - `occurredAt` string, date-time: When the error occurred
    - `accountTypes` object
    - `defaultEnforcementMode` 'STRICT' | 'AUDIT', required: Default enforcement for unmatched accounts; defaults to STRICT

## Other responses

- `400`: Bad request
- `401`: Authentication required (missing or invalid JWT token)
- `403`: Insufficient permissions (missing required scope)
- `405`: Method not allowed
- `409`: Conflict (e.g. idempotency key conflict, ledger deleted)
- `429`: Resource exhausted. Write admission can reject with `WRITES_BLOCKED_DISK_FULL` while disk pressure blocks writes; `SEQUENCE_EXHAUSTED` means an authoritative identifier allocator cannot allocate another identifier and is permanent for that allocator. Inspect `errorCode` before deciding whether to retry. No retry interval is supplied, and retrying cannot resolve sequence exhaustion.
- `500`: Internal server error
- `503`: Service unavailable — the server cannot serve the request right now but an immediate retry can succeed. The `errorCode` field discriminates the cause: `NO_LEADER` (no elected Raft leader), `CACHE_HORIZON_EXCEEDED` (admission snapshot too old, retry against a fresh one), `UNAVAILABLE` (an internal forwarded stream was torn down mid-transfer), or the reason of any domain error of the unavailable kind (e.g. `INDEX_BUILDING`).

## Changes

- **2026-11-03** `f5e817eca038`: 1 info
  - api operation id `createLedger` removed and replaced with `v3CreateLedger`
- **2026-10-09** `45c54ddbb457`: 7 warning, 10 info
  - removed the optional property `data/account_types` from the response with the `201` status
  - removed the optional property `data/created_at` from the response with the `201` status
  - removed the optional property `data/default_enforcement_mode` from the response with the `201` status
  - removed the optional property `data/deleted_at` from the response with the `201` status
  - …13 more
- **2026-10-08** `3da23d327316`: 3 info
  - added the media type `text/plain` for the response with the status `401`
  - added the media type `text/plain` for the response with the status `403`
  - added the non-success response with the status `429`
- **2026-09-30** `b747efd5cd77`: 1 info
  - endpoint added
- **2026-09-29** `6ff73ac9a6bd`: 1 breaking
  - api path removed without deprecation

[Operation history](https://skmtc.dev/formancehq/apis/ledger-api/changes/v3/:ledgerName/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)
