Skip to content

Create a manual ledger adjustment

The ledger.adjustments.create operation: POST /api/v1/ledger/adjustments in the REST API and the ledger_adjustments_create tool in the MCP server.

POST/api/v1/ledger/adjustments

Posts a manual adjustment to the legal entity's ledger: a transaction of kind adjustment that corrects balances, e.g. to record an opening bank balance. Posted transactions are never changed; to undo an adjustment, post the reverse one.

Send either two accounts and an amount (debitAccount, creditAccount, amount as { "amount": "<integer micros>", "currency": "EUR" }), or entries (2–50, each { account, side, amount, currency }) that balance in every currency. Every adjustment needs a memo: the reason, shown in the ledger, the audit log and to approvers.

  • Customer wallet accounts can't be adjusted (credit grants move them).
  • Accounts are codes from the chart of accounts, in the entry's currency. An adjustment may open entity-wide, bank (this entity's) and brand accounts (of this entity's brands); accounts that name a processor connection, product or customer must already exist.
  • brandId makes the transaction the brand's (default: the one brand whose accounts it moves).

Financial tier: the finance role (or an owner or admin) with a sign-in in the last 15 minutes. If the organization's approval policy lists this operation, a call above the limit of its currency (or in several currencies) answers approval_required: it runs once a second person approves it, as you sent it. Dry-run it first (Ownrecord-Dry-Run: true, dry_run: true) to see the entries it would post.

At a glance

  • REST: POST /api/v1/ledger/adjustments
  • MCP tool: ledger_adjustments_create
  • Operation ID: ledger.adjustments.create
  • Scope: Legal entity
  • Permission: ledger:adjust
  • Risk tier: financial
  • Dry run: Supported (send Ownrecord-Dry-Run: true)
  • Idempotency: Idempotency-Key required
  • Modes: Test and live
  • Authentication: API key with the ledger:adjust scope
  • Fresh sign-in: In the console, a sign-in within the last 15 minutes
  • Since: M2

Request

Body

A JSON object (Content-Type: application/json):

FieldTypeRequiredDescription
debitAccountstringNoTwo-account form: the account code to debit, e.g. bank:ent_…:EUR.
creditAccountstringNoTwo-account form: the account code to credit, e.g. opening_balances:EUR.
amountobjectNoTwo-account form: the amount, integer micros as a string, and its currency.
amount.amountstringYes
amount.currencystringYes
entriesarray of objectNoEntries form: 2 to 50 entries that balance in every currency.
entries[].accountstringYesAn account code from the chart of accounts, e.g. rounding:EUR.
entries[].sideone of debit, creditYesdebit or credit.
entries[].amountstringYesInteger micros as a string, more than zero.
entries[].currencystringYesThe account's currency.
brandIdbrd_… IDNoThe brand the adjustment belongs to. Default: the one brand whose accounts it moves.
memostringYesThe reason: shown in the ledger, the audit log and to approvers.

Response

201 Created with a JSON object:

FieldTypeDescription
idtxn_… ID
object"ledger_transaction"
entityIdent_… ID
livemodeboolean
brandIdbrd_… ID or null
kindone of 19 values, e.g. topup, sale, grant (all in the OpenAPI document)
statusone of pending, posted, voided
entriesarray of object
entries[].accountIdacct_… ID
entries[].accountCodestringThe account's code from the chart of accounts.
entries[].sideone of debit, credit
entries[].amountstringInteger micros, more than zero.
entries[].currencystring
totalsarray of objectDebit and credit totals per currency, always equal.
totals[].currencystring
totals[].debitsstring
totals[].creditsstring
effectiveAtdate-timeThe business date of the movement.
postedAtdate-time or null
voidedAtdate-time or null
reversalOftxn_… ID or nullThe posted transaction this one reverses.
linksobjectObjects the transaction is about, by ID.
memostringThe description; for a manual adjustment, its reason.
actorobjectWho posted it. approvalId names the approval request when a second person approved it.
actor.typeone of user, api_key, agent, app, system, public, customer
actor.idstring
actor.viaone of console, api, mcp, assistant, portal, job or null
actor.approvalIdstring or null
actor.onBehalfOfUserIdstring or null
actor.clientIdstring or null
actor.supportGrantIdstring or null
createdAtdate-time

Examples

Record an opening bank balance

Two accounts and an amount: debit the bank account, credit opening balances.

HTTP
POST /api/v1/ledger/adjustments
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "debitAccount": "bank:ent_01m4tx5nj8emwbyf7y9vcf47x2:EUR",
  "creditAccount": "opening_balances:EUR",
  "amount": {
    "amount": "1500000000",
    "currency": "EUR"
  },
  "memo": "Opening bank balance from the 2026-09-30 statement"
}

Response (201):

JSON
{
  "id": "txn_01m4tx5nj8emwbyf7y9vcf47xg",
  "object": "ledger_transaction",
  "entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
  "livemode": true,
  "brandId": null,
  "kind": "adjustment",
  "status": "posted",
  "entries": [
    {
      "accountId": "acct_01m4tx5nj8emwbyf7y9vcf47xc",
      "accountCode": "bank:ent_01m4tx5nj8emwbyf7y9vcf47x2:EUR",
      "side": "debit",
      "amount": "1500000000",
      "currency": "EUR"
    },
    {
      "accountId": "acct_01m4tx5nj8emwbyf7y9vcf47xd",
      "accountCode": "opening_balances:EUR",
      "side": "credit",
      "amount": "1500000000",
      "currency": "EUR"
    }
  ],
  "totals": [
    {
      "currency": "EUR",
      "debits": "1500000000",
      "credits": "1500000000"
    }
  ],
  "effectiveAt": "2026-10-14T09:30:00.000Z",
  "postedAt": "2026-10-14T09:30:00.000Z",
  "voidedAt": null,
  "reversalOf": null,
  "links": {},
  "memo": "Opening bank balance from the 2026-09-30 statement",
  "actor": {
    "type": "user",
    "id": "usr_01m4tx5nj8emwbyf7y9vcf47xa",
    "via": "console",
    "approvalId": "apr_01m4tx5nj8emwbyf7y9vcf47xj",
    "onBehalfOfUserId": null,
    "clientId": null,
    "supportGrantId": null
  },
  "createdAt": "2026-10-14T09:30:00.000Z"
}

Move usage revenue of one brand, with entries

HTTP
POST /api/v1/ledger/adjustments
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "entries": [
    {
      "account": "revenue:brd_01m4tx5nj8emwbyf7y9vcf47x4:usage:api.calls:EUR",
      "side": "debit",
      "amount": "2500000",
      "currency": "EUR"
    },
    {
      "account": "rounding:EUR",
      "side": "credit",
      "amount": "2500000",
      "currency": "EUR"
    }
  ],
  "memo": "Reclassify a rounding difference booked as usage revenue"
}

Response (201):

JSON
{
  "id": "txn_01m4tx5nj8emwbyf7y9vcf47xh",
  "object": "ledger_transaction",
  "entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
  "livemode": true,
  "brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
  "kind": "adjustment",
  "status": "posted",
  "entries": [
    {
      "accountId": "acct_01m4tx5nj8emwbyf7y9vcf47xe",
      "accountCode": "revenue:brd_01m4tx5nj8emwbyf7y9vcf47x4:usage:api.calls:EUR",
      "side": "debit",
      "amount": "2500000",
      "currency": "EUR"
    },
    {
      "accountId": "acct_01m4tx5nj8emwbyf7y9vcf47xf",
      "accountCode": "rounding:EUR",
      "side": "credit",
      "amount": "2500000",
      "currency": "EUR"
    }
  ],
  "totals": [
    {
      "currency": "EUR",
      "debits": "2500000",
      "credits": "2500000"
    }
  ],
  "effectiveAt": "2026-10-14T09:30:00.000Z",
  "postedAt": "2026-10-14T09:30:00.000Z",
  "voidedAt": null,
  "reversalOf": null,
  "links": {},
  "memo": "Reclassify a rounding difference booked as usage revenue",
  "actor": {
    "type": "user",
    "id": "usr_01m4tx5nj8emwbyf7y9vcf47xa",
    "via": "api",
    "approvalId": null,
    "onBehalfOfUserId": null,
    "clientId": null,
    "supportGrantId": null
  },
  "createdAt": "2026-10-14T09:30:00.000Z"
}

Errors

CodeHTTP statusWhenWhat to do
validation_failed400A field is missing or malformed, an entry's currency isn't its account's, both input forms were sent, or an account can't be used here (details.issues).Fix the fields listed in details.issues and send the request again.
ledger_unbalanced400Debits and credits differ in a currency (details.imbalances).Make the debits equal the credits in every currency (details.imbalances lists each currency's totals) and post again.
ledger_account_not_adjustable400An entry posts to a customer wallet account.Change wallet balances with a credit grant, an expiry or a refund instead, so the wallet's credit grants stay in step with its balance.
insufficient_funds402The adjustment would take a customer's credit balance below zero.Reserve or capture a smaller amount, or top up the wallet first. details.requested and details.available are in integer micros.
resource_not_found404brandId isn't a brand of this legal entity.Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.
idempotency_key_reused409The idempotency key was used before for another adjustment.Use a new Idempotency-Key for a different request, or resend the original request unchanged.
approval_required202The approval policy needs a second person for this amount; nothing was posted yet.Nothing ran yet. An approver reviews approval.summary and the dry-run result in the console (approval.approveUrl). Poll GET /approvals/{id} or watch for the approval.resolved event; sending the same request again with the same Idempotency-Key returns the same approval.
approval_rejected409The same request was sent again after it was rejected.Read the reason in details.approval.rejection. To try again, send a new request with a new Idempotency-Key.
approval_expired409The same request was sent again after its approval request expired.Send the request again with a new Idempotency-Key to create a new approval request. Requests expire after 72 hours.
stale_approval409The approved request could no longer run as requested.Check the current state (details.approval.failure says what failed), then send a new request with a new Idempotency-Key.
fresh_auth_required403Your last sign-in is older than 15 minutes.Sign in again, then repeat the action within 15 minutes.
livemode_not_allowed403You have no live-mode access, so you can't adjust the live ledger.Use test mode, or credentials for the target's mode. Developers need live access granted by an organization owner or admin.
scope_required400The request names no legal entity (an org key without Ownrecord-Entity).Call the operation with credentials or a target at its level: an organization, a legal entity, a brand or the platform (details.required_scope).
permission_denied403You can't adjust this entity's ledger (finance, owners and admins can).Ask an organization owner or admin for a role or scope that allows this action.
version_conflict409Another posting changed the same accounts at the same moment; retry.Fetch the object again and retry the change.

It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, idempotency_key_required, idempotency_in_progress, approval_policy_denied, rate_limited, internal_error.