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.
/api/v1/ledger/adjustmentsPosts 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.
brandIdmakes 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-Keyrequired - Modes: Test and live
- Authentication: API key with the
ledger:adjustscope - 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):
| Field | Type | Required | Description |
|---|---|---|---|
debitAccount | string | No | Two-account form: the account code to debit, e.g. bank:ent_…:EUR. |
creditAccount | string | No | Two-account form: the account code to credit, e.g. opening_balances:EUR. |
amount | object | No | Two-account form: the amount, integer micros as a string, and its currency. |
amount.amount | string | Yes | |
amount.currency | string | Yes | |
entries | array of object | No | Entries form: 2 to 50 entries that balance in every currency. |
entries[].account | string | Yes | An account code from the chart of accounts, e.g. rounding:EUR. |
entries[].side | one of debit, credit | Yes | debit or credit. |
entries[].amount | string | Yes | Integer micros as a string, more than zero. |
entries[].currency | string | Yes | The account's currency. |
brandId | brd_… ID | No | The brand the adjustment belongs to. Default: the one brand whose accounts it moves. |
memo | string | Yes | The reason: shown in the ledger, the audit log and to approvers. |
Response
201 Created with a JSON object:
| Field | Type | Description |
|---|---|---|
id | txn_… ID | |
object | "ledger_transaction" | |
entityId | ent_… ID | |
livemode | boolean | |
brandId | brd_… ID or null | |
kind | one of 19 values, e.g. topup, sale, grant (all in the OpenAPI document) | |
status | one of pending, posted, voided | |
entries | array of object | |
entries[].accountId | acct_… ID | |
entries[].accountCode | string | The account's code from the chart of accounts. |
entries[].side | one of debit, credit | |
entries[].amount | string | Integer micros, more than zero. |
entries[].currency | string | |
totals | array of object | Debit and credit totals per currency, always equal. |
totals[].currency | string | |
totals[].debits | string | |
totals[].credits | string | |
effectiveAt | date-time | The business date of the movement. |
postedAt | date-time or null | |
voidedAt | date-time or null | |
reversalOf | txn_… ID or null | The posted transaction this one reverses. |
links | object | Objects the transaction is about, by ID. |
memo | string | The description; for a manual adjustment, its reason. |
actor | object | Who posted it. approvalId names the approval request when a second person approved it. |
actor.type | one of user, api_key, agent, app, system, public, customer | |
actor.id | string | |
actor.via | one of console, api, mcp, assistant, portal, job or null | |
actor.approvalId | string or null | |
actor.onBehalfOfUserId | string or null | |
actor.clientId | string or null | |
actor.supportGrantId | string or null | |
createdAt | date-time |
Examples
Record an opening bank balance
Two accounts and an amount: debit the bank account, credit opening balances.
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):
{
"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
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):
{
"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
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | A 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_unbalanced | 400 | Debits 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_adjustable | 400 | An 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_funds | 402 | The 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_found | 404 | brandId 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_reused | 409 | The idempotency key was used before for another adjustment. | Use a new Idempotency-Key for a different request, or resend the original request unchanged. |
approval_required | 202 | The 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_rejected | 409 | The 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_expired | 409 | The 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_approval | 409 | The 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_required | 403 | Your last sign-in is older than 15 minutes. | Sign in again, then repeat the action within 15 minutes. |
livemode_not_allowed | 403 | You 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_required | 400 | The 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_denied | 403 | You 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_conflict | 409 | Another 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.