# Error codes

> Every error code the API, the MCP server and the console can return, with its HTTP status and what to do next.

Every error has a stable `code`, a `type`, a `message`, a `remediation` that says what to do next, and whether retrying can succeed. The `docs_url` of an error links to its entry on this page. See [Errors](https://ownrecord.io/docs/reference.md#errors) for the response body.

## Types

| Type                 | HTTP status   | Codes |
| -------------------- | ------------- | ----- |
| `invalid_request`    | 400, 405, 415 | 132   |
| `authentication`     | 401           | 10    |
| `permission`         | 403           | 22    |
| `entitlement`        | 403           | 4     |
| `not_found`          | 404           | 11    |
| `conflict`           | 409           | 152   |
| `rate_limited`       | 429           | 7     |
| `tax_blocked`        | 422           | 6     |
| `insufficient_funds` | 402           | 3     |
| `approval_required`  | 202           | 1     |
| `processor_error`    | 502           | 10    |
| `internal`           | 500           | 12    |

## Invalid request

### `agent_role_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

An agent can't hold this role at this scope. Choose a role other than org_owner, at a scope the role allows (org, entity or brand; see the role table in the docs).

### `amount_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The amount isn't a valid decimal amount for this currency. Write amounts in major units as decimal strings with at most 6 decimals, e.g. "12.50". Where a whole number of the currency's minor unit is required (payments, invoice totals), use at most its minor-unit decimals (EUR 2, JPY 0, BHD 3).

Amounts are exact integers in micros (millionths of the major unit). A decimal string is parsed without floating point; anything finer than a micro, or than the minor unit where one is required, is refused instead of rounded silently.

### `api_key_scope_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

One or more scopes can't be put on this kind of API key. Use permission scopes from the catalogue ({resource}:{read|write|admin}, e.g. customers:read) that this kind of key can use. `details.problems` lists each scope and why.

Brand keys can use brand resources (customers, subscriptions, usage, …); entity keys also the entity's
ledger, filings, registrations and tasks; org management keys also members, security, audit, approvals,
agents and API keys. On platform data, keys can only read tax knowledge (`tax_knowledge:read`).
`details.problems[]` has `scope`, `reason` (`unknown_permission`, `kind_not_allowed`,
`platform_not_allowed`) and `message`.

### `api_version_unsupported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This API version doesn't exist. Send a supported version in `Ownrecord-Version` (`details.supported_versions`), or leave the header out to use your account's pinned version.

### `app_listing_incomplete`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The listing application is incomplete. Answer every required question of the current security questionnaire, justify every scope the app asks for, and give a privacy policy URL. `details.problems` lists what's missing.

An application to list an app names the publisher, links its privacy policy, answers the current version of the
security questionnaire (`apps.listing.get` returns its questions) and justifies each scope the app asks for. A
reviewer can't approve an app without a complete application either. Each entry of `details.problems` has a
`path` and a `message`.

### `app_not_installable`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This app can't be installed in this organization. Ask the app's publisher to list it: private apps install only in the organization that registered them, and other organizations can install a listed app once platform review has approved it.

### `app_review_checklist_incomplete`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Tick every required checklist item to approve. Check each required item of the review checklist and tick it, or request changes or reject with a reason instead. `details.missing` lists the items.

### `assistant_action_unsupported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The assistant can't run this operation. Do it on its page in the console, or through the API. The assistant runs only operations it can preview with a dry run, and never ones that create or reveal secrets.

### `assistant_batch_too_large`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Too many items for one action card. Split the change into smaller batches (`details.max` items per card).

### `assistant_injection_suspected`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Data on this page contains text that looks like instructions, so the assistant won't propose changes in this answer. Nothing was changed. If you want a change, ask for it directly in a new message, and check the card's preview before you confirm.

Customer-entered text (names, notes, addresses, metadata) is data, never instructions (spec §43.5). When a
tool result contains text that reads like instructions to the assistant, that text is withheld from the
model and the assistant proposes no action for the rest of the answer.

### `assistant_page_context_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The assistant doesn't know which organization you're working in. Open a page of the organization in the console (or turn the page context on for this message), then ask again.

### `authorization_document_not_found`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The document isn't an authorization document of this legal entity. Upload the signed document with `authorizations.documents.upload` and pass its `file_…` ID as `documentFileId`.

### `authorization_secret_refused`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This looks like a credential. Ownrecord never stores government portal credentials. Keep passwords, PINs, eIDs and certificates in your own vault. Record only non-secret notes: the access method, who holds access and the renewal date. `param` names the field.

### `bank_statement_amount_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The line's amount isn't the invoice's amount due. Match the line to the invoice whose amount due it pays exactly (same currency). Partial payments and overpayments stay in the review queue: resolve them with the customer, then ignore the line with a reason.

Invoices are paid in full only (docs/spec-questions.md #429), so a bank line pays an invoice only when its amount equals the amount due. `details.line_amount` and `details.amount_due` are the two amounts.

### `bank_statement_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The bank statement file can't be read. Fix the rows listed in `details.errors` (or name the columns, date format and decimal separator) and import the whole file again. Nothing was imported.

The importer reads CSV files with a header row: a booking date, a signed amount (incoming money positive), a reference and optionally the counterparty and a currency (spec §16.9; CAMT.053 files arrive in v1.1). Columns are found by common header names or named in `columns`. A file with any unreadable row is refused whole, so a corrected file imports cleanly: `details.errors` lists up to 20 problems as `{ row, column, message }`.

### `capture_exceeds_reservation`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The capture amount is more than is still reserved. Capture at most the reserved amount that is still open (`details.outstanding`, integer micros), or reserve more first.

### `checkout_amount_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

A line item's quantity or amount isn't valid for its price. Give a quantity from 1 to 1,000 for fixed prices; for a custom-amount price give `customAmount` within the price's minimum and maximum, in whole minor units of its currency, with quantity 1. `param` names the line item.

### `checkout_business_details_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

A business purchase needs the company name and its full address. Give the company name and the billing address (street, city, country) when buying as a business (spec §13.2).

### `checkout_currency_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The line items use different currencies. Put prices of one currency in a session; create one session per currency.

### `checkout_mode_not_supported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Checkout doesn't sell this mode yet. Create the session with `mode: payment` (one-time prices and credit packs), `subscription` (recurring prices) or `setup` (save a payment method). Top-ups arrive with milestone M6 (`details.available_from`).

The session schema stores every mode of §7.6 (`payment`, `subscription`, `setup`, `topup`); checkout sells `payment`, `subscription` and `setup` sessions, and `topup` from the milestone that builds it (`details.available_from`).

### `checkout_price_not_sellable`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This price can't be sold at checkout. Sell one-time prices and credit packs in `payment` mode and recurring prices in `subscription` mode. `param` names the line item.

### `checkout_promotion_code_not_allowed`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This checkout doesn't take promotion codes. Create the session with `allowPromotionCodes: true`, or with `promotionCode` to apply one for the customer. Promotion codes apply to `subscription` sessions.

### `checkout_quantity_not_adjustable`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This line item's quantity can't be changed here. Only line items created with `adjustableQuantity` take a new quantity on the hosted page, within its `min` and `max`.

### `checkout_session_expiry_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The session's expiry time isn't allowed. Set `expiresAt` between 30 minutes and 24 hours from now, or leave it out for 24 hours.

### `checkout_terms_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The customer hasn't accepted the terms. Ask the customer to accept the brand's terms and, for digital services delivered at once to EU or UK consumers, to agree to immediate performance and the loss of the withdrawal right (`details.missing`).

### `confirmation_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The confirmation doesn't match. Type exactly what the form asks for (the organization's slug, or your email address).

### `conversation_scope_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This conversation belongs to another organization or console. Start a new conversation here; a conversation never moves between organizations.

### `currency_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The amounts are in different currencies. Use amounts in the same currency. Prices are set explicitly per currency; nothing is converted automatically.

Money arithmetic never mixes currencies (spec §8.1). `details.currencies` lists the currencies involved.

### `customer_deleted`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This customer was deleted. Create a new customer for this buyer. Deleted customers stay readable for the records they belong to, but can't be changed.

Deleting a customer is a soft delete (`deletedAt`): invoices, payments and evidence keep pointing at it, and it can still be read, but nothing about it changes any more.

### `customer_tax_id_limit`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The customer already has the maximum number of tax IDs. Remove a tax ID the customer no longer uses before adding another (at most 5 per customer).

### `dev_login_not_allowed`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Dev login only accepts @ownrecord.test addresses. Sign in as one of the seeded @ownrecord.test users listed on /dev.

Development and test only (spec §6.7.2). The `.test` domain can't receive email, so no real account can be impersonated.

### `dispute_assignee_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This person can't be assigned the dispute. Assign a member of the organization who may work on this brand's disputes in this mode (support, admins and owners at the organization, the entity or this brand). `details.userId` is the ID given.

### `dispute_evidence_unsupported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This processor doesn't take dispute evidence through its API. Download the evidence pack from the console and submit it in the processor's own dashboard. `details.processor` names the processor.

### `domain_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This isn't a host name a brand can use as its domain. Enter one host name you registered, such as pay.acme.com: no https\://, path, port or wildcard, and not an IP address.

`details.reason` says why: `empty`, `url` (a scheme or path was included), `port`, `wildcard`, `format`, `too_long`, `single_label`, `ip_address`, `special_use` (reserved names such as `.local`, `.test` or `example.com`, which can never point at Ownrecord) or `reserved` (Ownrecord's own domains and its hosting provider's). Also returned when the domain provider refuses the name.

### `dry_run_not_supported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This operation doesn't support dry runs. Send the request without `Ownrecord-Dry-Run` (REST) or `dry_run` (MCP).

### `einvoice_document_unsupported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This document can't be expressed as an EN 16931 e-invoice. Read the message for the reason. Issue taxed lines and lines not subject to VAT on separate documents, make sure a reverse-charge buyer has a VAT ID, and use a currency with at most two decimals.

Raised when an issued invoice or credit note can't be written as EN 16931 UBL (Peppol BIS Billing 3.0): lines
not subject to VAT mixed with taxed lines (BR-O-11), a reverse-charge document without the buyer's VAT ID
(BR-AE-02), a seller without a VAT ID on a document with VAT, a currency with more than two decimals, a buyer
without a country, or a snapshot whose totals don't equal its lines. The e-invoice is marked `failed` and an
`EINVOICE_REJECTED` alert is raised; the document itself stands.

### `einvoice_endpoint_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This isn't a valid electronic address. Write it as \<scheme>:\<identifier> with a scheme from the Peppol list of electronic address schemes, e.g. 9934:HR12345678903 or 0208:0123456789.

Electronic addresses are Peppol participant identifiers: a four-digit EAS scheme code and the identifier, without spaces.

### `einvoice_endpoint_missing`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

A party of the e-invoice has no electronic address. For the buyer, set the customer's `einvoice_endpoint` metadata to its Peppol address (\<scheme>:\<identifier>, e.g. 0208:0123456789), then retry the e-invoice. For the seller, set the entity's Peppol address in its e-invoicing settings.

Peppol requires both electronic addresses (PEPPOL-EN16931-R010, -R020). Ownrecord derives them from VAT IDs
where the Peppol code list has a VAT scheme for the country (e.g. 9934 for Croatia); consumers, and businesses
in countries whose VAT numbers aren't Peppol addresses (Sweden, Finland, Denmark), need an explicit address.
`details.party` is `buyer` or `seller`.

### `einvoice_provider_unavailable`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This e-invoicing provider isn't available here. Choose a provider that is available in this environment. The fake provider exists only in development and test; certified providers are added with the product owner (spec §35 #8).

The `fake` provider is a development and CI tool: it is refused unless the development gate is open, so a real
deployment never pretends to have sent an e-invoice. `details.provider` names the provider.

### `einvoice_provider_unknown`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

There is no e-invoicing provider with this ID. Use one of the providers `einvoice.settings.get` lists under `providers`.

`details.provider` names the ID that was given.

### `einvoice_provider_unsupported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The provider doesn't deliver e-invoices for this country and transaction type. Choose another provider for this country and transaction type, or remove the route. `providers[].countries` in `einvoice.settings.get` says what each provider covers.

`details` names the provider, the country and the transaction type (`b2b`, `b2g` or `b2c`).

### `einvoice_routes_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The e-invoicing routes aren't valid. Give each country and transaction type at most one provider, with ISO 3166-1 country codes and the types b2b, b2g or b2c.

`param` points at the route that is wrong.

### `email_domain_blocked`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Accounts can't be created with a disposable email address. Sign up with a permanent address, for example your work email.

New accounts need an address from a permanent email provider. Existing accounts are not affected.

### `email_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This email address isn't valid. Check the address and try again.

### `entitlement_feature_not_found`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This brand has no feature with this key in this mode. Check the feature key against `features.list` (test and live mode have separate features), or create the feature first with `features.create`.

Entitlements are values of the brand's own features (spec §36.1). `param` names the field with the key (`feature` for `entitlements.check`, `featureKey` for an override) and `details.feature_key` repeats it.

### `entitlement_value_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

An entitlement value doesn't fit its feature. Use true or false for boolean features, a whole number of 0 or more for limits, and one of the listed values for enum features.

`param` is the first wrong value; `details.issues` lists every problem with its path and message.

### `export_period_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The export period isn't valid. Give `from` and `to` as calendar dates (YYYY-MM-DD) in the entity's time zone, `from` on or before `to`, at most 366 days in all, and not ending in the future.

### `feature_not_found`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The product names a feature this brand doesn't have. Create the feature first (`features.create`), or fix the feature key.

A product's entitlements are values of the brand's own features, in the same mode (spec §36.1). `param` is the first unknown entry, `details.feature_keys` lists every unknown key.

### `filing_receipt_not_found`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The receipt isn't a document of this filing period. Upload the receipt to the period first (`filing_periods.documents.upload`) and pass its `file_…` ID, or pass a reference instead.

A submission's receipt is a document uploaded to the same filing period, or the operator's reference to a document kept elsewhere. `param` names the field.

### `filing_schedule_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This filing schedule can't generate filing periods. Give a due rule (`due`) when the regime's filing spec has none Ownrecord can compute (`details.problem`: `due_other`), or complete it (`due_days_missing`, `due_nth_day_missing`, `due_months_missing`). A regime without a filing spec (`no_filing_spec`) needs its filing rules in the knowledge base first.

A registration's own filing schedule (WP-M8-filing-reports) sets the frequency, the stagger and, where the filing spec's due rule can't be computed (e.g. the UK's "1 calendar month and 7 days"), the due rule. The filing spec is still needed for the filing currency and the report. `details` has `regime_id` and `problem`.

### `gl_mapping_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The general-ledger mapping isn't valid. Fix the rules listed in `details.problems` (`index`, `field`, `message`): `account` is an account kind (e.g. `tax_payable`), an account code, or a code prefix ending in `*` (e.g. `tax_payable:EU_OSS_UNION:*`), each at most once; `glAccount` is 1–40 letters, digits, and `.`, `-`, `_`, `/` or spaces inside; at most 200 rules.

### `idempotency_key_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

An Idempotency-Key header is required on POST requests. Send a unique Idempotency-Key header (for example a UUID) with every POST.

### `installer_not_eligible`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Apps are installed and owned by organization owners and admins. Re-assign the app to an org_owner or org_admin of this organization.

### `invalid_json`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The request body must be a JSON object. Send the input as a JSON object, e.g. `{}` when there is nothing to send.

### `invoice_currency_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This item's currency isn't the invoice's. Every item of an invoice is in the invoice's currency (`details.invoice_currency`). Create a separate draft for the other currency.

### `invoice_item_price_unusable`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This price can't be used on an invoice item. Use an active one-time price with a fixed amount, or give the product (`productId`) and a `unitAmount` instead. Recurring prices are billed by subscriptions and credit packs by checkout.

### `invoice_lines_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The invoice lines don't match the tax quote. Describe every line of the quote exactly once, with a description of 1 to 500 characters and a whole-number quantity from 1 to 1,000,000. `param` names the line.

### `invoice_number_format_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The legal entity's invoice number format can't produce unique invoice numbers. Change the entity's invoice number format (Invoice settings) to a template with {series}, {seq} and, when the sequence restarts every year, {year}, e.g. {series}-{year}-{seq:06}.

Invoice numbers are the entity's template filled with the brand's series, the issue year and the sequence number (spec §18.2). The template may use `{series}`, `{year}`, `{seq}` and `{seq:NN}` (zero-padded). It must contain `{series}` (the brands of an entity share the template) and `{seq}`, and `{year}` when `sequenceResetYearly` is on. Country templates with fiscal elements (Croatian business-premises and device codes) arrive with fiscalisation. `details.number_format` has the template.

### `invoice_series_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The invoice series isn't valid. Use a series of uppercase letters, digits and inner hyphens, at most 30 characters, e.g. GEN or GEN-CN (Brand settings → Invoice series).

### `lease_usage_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The usage summary doesn't add up to the consumed amount. Make the amounts of the `usageSummary` lines add up to `consumed` exactly (`details.consumed`, `details.usageTotal`, integer micros), in the lease's currency.

A settle captures `consumed` and books it as usage revenue per meter from the `usageSummary` lines (spec §17.3, §11.3), so the lines must account for every micro of it. Lines may have a zero amount (usage that cost nothing).

### `ledger_account_code_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This isn't a valid ledger account code. Use an account from the chart of accounts, e.g. `processor_clearing:pcon_…:EUR` or `customer:cus_…:wallet:EUR:available` (see `details.code`).

Account codes follow the chart of accounts (spec §11.2): one ledger per legal entity, one account per code and currency. Codes end with the ISO 4217 currency, except wallet accounts, which carry it after `wallet` (`customer:{cus}:wallet:{currency}:available|reserved|promo`). IDs inside a code must be TypeIDs of the right kind; meter names use letters, digits, `.`, `_` and `-`.

### `ledger_account_not_adjustable`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Manual adjustments can't post to customer wallet accounts. 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.

A wallet's balance must always equal the remaining amount of its credit grants (spec §11.5, invariant 4). A manual adjustment can't keep that true, so it may not post to `customer:…:wallet:…` accounts. `details.account` is the refused account code.

### `ledger_posting_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The ledger posting is invalid. Fix the entry or amount named in `details` (`details.reason`, and `details.index` for an entry) and post again.

`details.reason` is one of: `empty` (no entries), `non_positive_amount` (every entry amount must be a positive number of micros), `currency_mismatch` (an entry's currency differs from its account's currency), `negative_amount` (an input amount such as a tax line is below zero), `inconsistent_amounts` (the amounts given for a posting don't add up, e.g. usage lines that don't sum to the captured amount), `memo_required` (manual adjustments need a reason) or `rounding_too_large` (a rounding residue of one minor unit or more).

### `ledger_unbalanced`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The ledger entries don't balance: debits and credits differ in at least one currency. Make the debits equal the credits in every currency (`details.imbalances` lists each currency's totals) and post again.

Every ledger transaction must balance per currency (spec §11.5, invariant 1). `details.imbalances[]` has `currency`, `debits` and `credits` in integer micros. Ownrecord's own postings are built from templates that always balance, so you see this code only for manual adjustments.

### `legal_version_not_acceptable`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This version of the document can't be accepted now. Accept the version in effect, or an announced newer version. `details.acceptable` lists the versions you can accept; read them at /legal/{document}/{version}.

An organization accepts the version of the Terms of Service or the DPA that is in effect, or an announced version ahead of its effective date. Earlier versions, versions that aren't announced yet and unknown versions are refused. `details.document` names the document and `details.acceptable` the versions that can be accepted now.

### `live_keys_unavailable`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Live API keys can't be created yet. Create a test-mode key, or complete the brand's go-live checklist (console → brand → Go live) and try again.

Live keys need: the organization's plan to include live mode, the organization to be active (not
restricted, suspended or closing), and a live brand: the key's own brand for a brand key, any brand of the
entity or organization for entity and org keys (go-live checklist, spec §38.3).
`details.blockers[]` lists `org_plan`, `org_status`, `brand_not_live` or `no_live_brand`.

### `livemode_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The object belongs to the other mode (test or live). Use credentials for the same mode as the object.

### `method_not_allowed`

**HTTP status:** 405 · **Type:** `invalid_request` · **Retryable:** no

This endpoint doesn't support this HTTP method. Use one of the methods in the `Allow` response header (`details.allowed_methods`).

Answered with HTTP 405. The path exists, but not with this method.

### `mode_not_supported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This operation isn't available in this mode (test or live). Use the mode the operation supports (`details.supported_mode`): test-only operations such as test clocks need test-mode credentials.

### `oauth_grant_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The authorization doesn't belong to you and this client. Start the authorization again from the application.

Consent is recorded on the grant of the signed-in user and the client that asked; anything else is refused.

### `onboarding_step_not_manual`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This onboarding step is done when the organization's data shows it, not by marking it. Do the step (for example, create the brand), or skip it. Only the registrations review is marked done.

### `org_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This access token covers several organizations: name the one to act in. Send the `Ownrecord-Org: org_…` header with the organization's ID.

A token from an app installed in several organizations by the same person, or from an MCP client the user
connected to several organizations, needs `Ownrecord-Org` on every call (spec §41.3).

### `platform_billing_hold_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This hold can't be set. Give a reason, a stage (`active`, `restricted` or `suspended`) and, to end it automatically, an `until` in the future.

### `platform_billing_thresholds_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

These escalation thresholds can't be used. Use whole days from 1 to 60, with `suspendAfterDays` later than `restrictAfterDays` (the defaults are 14 and 30).

### `platform_plan_unknown`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

There is no Ownrecord plan with this key that you can choose. Pick one of the plans `platform_billing.get` lists under `plans` (enterprise is arranged with sales; internal is never sold).

### `platform_staff_domain_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Platform roles are only for addresses at the platform's staff domains. Use the person's address at one of the staff domains (`PLATFORM_STAFF_DOMAINS`, shown on the Staff page).

### `platform_user_self`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

You can't disable your own account. Ask another platform admin to do it.

### `processor_credentials_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The processor rejected the API key. Copy the key again from the processor's dashboard (Stripe: Developers → API keys). It may have been rolled or deleted.

Returned when connecting or rotating a key that the processor doesn't accept. Nothing was stored.

### `processor_key_mode_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The key belongs to the other mode. Use a test key (Stripe r&#x6B;_&#x74;est_…, Mollie test\*…) for a test-mode connection and a live key (rk_live\*…, live\_…) for a live-mode connection, or switch the console's mode.

Test mode and live mode are separate worlds (spec §6.1): a connection's key must match the mode it is created in. `details.key_livemode` says which mode the key is for.

### `processor_key_not_restricted`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Live connections need a restricted key. Create a restricted key (r&#x6B;_&#x6C;ive_…) with the permissions listed in the docs instead of using the account's secret key (s&#x6B;_&#x6C;ive_…).

A restricted key limits what Ownrecord can do on the account to what it needs (spec §15.2). Test mode also accepts a secret test key (s&#x6B;_&#x74;est_…) for development.

### `processor_not_available`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This payment processor isn't available here. Connect Stripe or Mollie. The fake processor exists only in development and test.

The `fake` processor is a development and CI tool: it is refused unless the development gate is open (a
development or test deployment with development access enabled), so a real deployment can never take a fake
payment or accept a fake webhook. `details.processor` names the processor.

### `processor_not_configured`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

No processor connection can take payments for this brand in this mode. Connect a processor to the brand's legal entity in this mode (console → entity → Processor connections), or reactivate a disconnected one.

A brand uses its primary connection, or its secondary while the primary is degraded (spec §15.4). `details` has `brand_id` and `livemode`.

### `processor_permissions_missing`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The restricted key is missing permissions Ownrecord needs. Edit the restricted key in the processor's dashboard and grant the permissions listed in `details.missing_permissions`, then try again.

Stripe restricted keys need write access to PaymentIntents, SetupIntents, PaymentMethods, Customers, Refunds,
Disputes and Webhook Endpoints, and read access to Balance and Payouts. `details.missing_permissions` lists
what the key lacks as `resource:access` (e.g. `refunds:write`). Nothing was stored.

### `processor_webhook_url_not_public`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Stripe can't deliver webhooks to this deployment's URL. On a development machine, run `stripe listen --print-secret`, pass the secret it prints (whsec\_…) as `webhookSecret`, then forward events with `stripe listen --forward-to` the connection's webhook URL.

Ownrecord creates the webhook endpoint on the Stripe account itself (spec §15.2), which needs a public `https`
address. A local deployment (`http://localhost:…`) can't receive Stripe's webhooks directly: forward them with
the Stripe CLI, whose signing secret stays the same between runs, and give that secret instead.
`details.webhook_url` is the address Stripe would have been given.

### `proposal_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The confirmation doesn't match the proposed action. Confirm from the action card itself: the confirmation must name the card's proposal and input hash. Nothing ran.

### `reconciliation_connection_unsupported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This processor connection can't be reconciled. Reconciliation reads a connection's balance transactions: the connection must be active (not disconnected) and in the mode of the request. `details.status` is the connection's status.

### `reconciliation_payout_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This bank line can't confirm that payout. Pick a payout item (`kind: payout`) of this entity and mode that didn't fail and isn't confirmed by another line, for exactly the line's amount and currency (`details` says what differs). Bank fees deducted from a payout aren't matched automatically: resolve the line with a note and book the fee with a ledger adjustment.

### `refund_amount_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The refund amount isn't valid. Give a positive amount in the invoice's currency, in whole minor units (integer micros as a string, e.g. 12500000 for 12.50).

### `refund_type_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This refund type doesn't fit the payment. Use `unused_credits` for credit packs and top-ups, `subscription_prorated` for a subscription invoice with a billing period, `partial` with an `amount`, or `full`. `details.reason` says what didn't fit.

`full` credits everything that is left on the invoice (credit packs only up to their unused credits); `partial` credits `amount` across the invoice's lines; `unused_credits` credits the unused credits of the invoice's credit packs; `subscription_prorated` credits the unused time of the invoice's licensed periods. An `amount` is given with `partial` and only with it.

### `registration_details_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The registration details are incomplete. Fill in the field named in `param`: the registration number, the date it is effective from, or at least one proof file.

Marking a registration registered needs the registration number, its effective date and proof (spec §10.6); marking the application submitted needs proof of the submission. `param` and `details.field` name the missing value.

### `registration_event_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The registration event is incomplete. This is a problem on our side. Contact support with the request ID.

An internal caller passed a registration event without the data it needs, e.g. a regime change without the entity's establishment. `details.problem` says what is missing.

### `registry_extract_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The registry extract must be a PDF, PNG or JPEG of at most 10 MB. Upload the extract from the company register as a PDF (or a clear photo) under 10 MB.

### `risk_evaluation_not_held`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This payment wasn't held for review. Only payments the risk rules held are in the review queue (`risk.reviews.list`). Refund other payments with `refunds.create`.

### `risk_payment_declined`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This payment can't be accepted. Use another payment method, or contact the seller. The seller's team sees why in the console (Brand → Risk); the answer never says.

The brand's risk rules refused the payment before it reached the card (spec §20): a blocklist, a velocity limit, a new customer's top-up limit, a disposable email address or a high risk score. The answer is the same whatever the rule, so it tells nobody how to get around it. Operators read the reasons with `risk.evaluations.get` (or the list filtered by `checkoutSessionId`).

### `scope_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This operation needs a different scope than the request has. Call the operation with credentials or a target at its level: an organization, a legal entity, a brand or the platform (`details.required_scope`).

Every operation runs at one level: `org`, `entity`, `brand` or `platform` (shown in the API reference). Brand operations need a brand (and its entity), entity operations need an entity. A broader target such as an organization management key must name the entity or brand to act on. `details.required_scope` is the level the operation needs.

### `slack_webhook_url_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This Slack webhook URL can't be used. Use the incoming-webhook URL Slack (or a Slack-compatible tool) gave you: https, a public host, no user name or password. `details.reason` says what is wrong.

Webhook URLs follow the same rule as outbound webhooks (spec §6.8): https only, public hosts only (no private,
loopback or link-local addresses), no credentials or fragment, at most 2,048 characters. On a development
machine with the dev gate open, http to a loopback host is allowed for a local fake.

### `slug_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Slugs are 3 to 40 lowercase letters, digits and single hyphens, and can't be a reserved word. Choose a slug such as `acme` or `acme-app`. Reserved words such as `api`, `console` and `docs` can't be used.

`details.reason` is `format` or `reserved`.

### `subscription_anchor_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The billing cycle anchor isn't allowed. Set `billingCycleAnchor` after the subscription starts (after the trial, if any) and at most one of its longest interval later, or leave it out to bill from the start.

### `subscription_change_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This change can't be made this way. Check `details.reason` and adjust the request.

`details.reason` is `no_change` (nothing would change), `collection_method_not_supported` (`pending_if_incomplete` needs `charge_automatically`) or `pending_payment_update` (a change is waiting for its invoice to be paid: pay or void that invoice first).

### `subscription_item_meter_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This subscription item doesn't bill this customer's usage of this meter. Name the customer's live metered item for the event's meter, or leave out `subscriptionItemId`: usage is billed to the customer's metered item for the meter.

`details.reason` is `customer` (the item belongs to another customer), `ended` (its subscription is canceled or expired), `not_metered` (a licensed item) or `meter` (it bills another meter, `details.item_meter`).

### `subscription_migration_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

These prices can't be used for a price migration. Migrate between two recurring prices that bill alike: the same currency, interval and usage type (and meter, for metered prices). The new price must be active.

`details.reason` is `same_price`, `currency_mismatch`, `interval_mismatch`, `usage_type_mismatch`, `meter_mismatch`, `not_recurring` or `archived` (the new price or its product is archived).

### `subscription_migration_too_large`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Too many subscriptions match this price migration. Narrow it with `filter.customerIds` or `filter.statuses`, and run several migrations. One migration schedules at most 1,000 subscriptions.

`details.matched` is how many subscriptions matched and `details.max` the limit.

### `subscription_payment_method_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This payment method can't be used for the subscription. Use a saved payment method (`pm_…`) of the subscription's customer that isn't detached, or leave it out to use the customer's default.

### `subscription_price_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This price can't be used on this subscription. Use active recurring prices of one currency whose intervals are whole multiples of each other (monthly with yearly, weekly with 2-weekly). `details.reason` says what's wrong and `param` names the item.

`details.reason` is `not_recurring` (a one-time price or credit pack), `archived` (the price or its product is archived; existing items keep billing, new items need an active price), `currency_mismatch` (every item of a subscription bills in one currency) or `intervals_incompatible` (a month-based and a day-based interval, or two intervals that don't divide each other, such as 2 and 3 months).

### `subscription_refund_not_supported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The unused time can't be refunded to the customer's card. An invoice of the current period wasn't paid by card (bank transfer or the credit balance; `details.invoice_id`). Cancel with `prorate: true` to credit the unused time to the customer's credit balance, or without it.

### `task_assignee_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This person can't be assigned the task. Assign a member of the organization who may work on tasks for this legal entity in this mode (an owner, an admin, a tax manager or finance, at the organization or this entity).

### `task_authorization_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This authorization can't back this submission. Use an authorization of this legal entity and mode that is valid now and covers the task's regime. `details.problem` is `revoked`, `expired`, `not_yet_valid`, `regime_not_covered`, `other_entity` or `other_mode`.

### `task_authorization_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Submitting this task needs an authorization to act for the entity. Record who may act for the entity under this regime (`authorizations.create`) and pass its ID as `authorizationId`. Ownrecord records the authorization; it doesn't grant it.

### `task_complete_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This task doesn't change a registration or a filing period: complete it with `tasks.complete`. Call `tasks.complete` with the task's outputs.

### `task_output_file_not_found`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

A receipt file in the outputs isn't a document of this task. Upload the receipt to the task first (`tasks.documents.upload`) and pass its `file_…` ID, or record the receipt number as a reference instead. `details.fileId` is the ID that wasn't found.

### `task_submit_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This task changes a registration or a filing period: submit it with `tasks.submit`. Call `tasks.submit` with the task's outputs (and an authorization for `*.submit` tasks). It is a compliance-tier operation, so your approval policy may ask a person to approve it first.

### `tax_id_check_not_available`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This tax ID can't be checked with a public register. Upload an extract from the company register instead: Ownrecord reviews it and verifies the entity.

VIES checks EU VAT IDs, HMRC checks UK VAT numbers and the UID register checks Swiss UIDs. Other tax IDs, and a number that isn't well-formed, are verified from a registry extract (`entities.registry_extract.upload`).

### `tax_id_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The tax ID isn't valid. Check the number and its country for typing errors (`details.reason` says what is wrong), then send it again. A Northern Ireland business enters its GB VAT number.

The number failed the checks Ownrecord makes before asking a register (spec §9.4). `details.reason` is one of: `format` (not the format of a tax ID of that type and country), `checksum` (the check digit is wrong; checked for Croatian OIB-based VAT IDs, Swiss UIDs and Norwegian organisation numbers), `country_mismatch` (the number carries another country's prefix), `unsupported_country` (the type doesn't exist in that country, e.g. `eu_vat` for Switzerland) or `xi_prefix` (Northern Ireland XI numbers aren't accepted; use the GB number). Spaces, dots and hyphens are ignored, and the country prefix may be left out (`DE` + `123456789`).

### `tax_id_type_unsupported`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This tax ID type isn't supported. Use one of the types in `details.supported_types`: `eu_vat` (EU VAT ID), `gb_vat` (UK VAT number), `ch_uid` or `ch_vat` (Swiss UID or VAT number), `no_org` (Norwegian organisation number) or `us_ein` (US EIN).

Ownrecord checks EU VAT IDs with VIES, UK VAT numbers with HMRC and Swiss VAT numbers with the UID register; Norwegian organisation numbers and US EINs are checked by format (spec §9.4).

### `tax_input_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The tax quote input is inconsistent. Fix the field named in `param` and quote again: every line needs a unique ID, the same currency and a whole number of the currency's minor unit.

Thrown by the tax engine before it quotes: no lines, duplicate line IDs, mixed currencies, line amounts finer than the currency's minor unit, or a seller that doesn't match the entity's tax state.

### `test_clock_advance_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The test clock can't advance to this time. Advance to a time after the clock's `frozenTime`, at most two years ahead per advance (advance several times to go further).

`details.reason` is `not_forward` (the time isn't after the clock's) or `too_far` (more than 732 days ahead; `details.latest` is the latest allowed time).

### `test_clock_customer_limit`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This test clock already has the most customers it can have. Create another test clock for more customers. A clock holds at most 10 customers.

### `test_clock_mode`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

Test clocks belong to test mode. Attach test clocks to test-mode customers only. Live customers bill at the real time.

### `unsupported_media_type`

**HTTP status:** 415 · **Type:** `invalid_request` · **Retryable:** no

The request body must be JSON. Send `Content-Type: application/json` with a JSON body.

Answered with HTTP 415.

### `usage_meter_billed_in_arrears`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

A metered price bills this meter in arrears, so wallet usage can't use it. Report prepaid (wallet) usage on a meter that no metered price names, e.g. `api.calls.prepaid` (`details.meter`).

Prepaid wallet usage and usage billed in arrears can coexist for a customer on different meters (spec §17.4). Usage of a meter that a metered price of the brand names (in any state) is billed on the renewal invoice, so paying it from a wallet as well would charge it twice.

### `usage_meter_unknown`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

No metered price of this brand uses this meter. Check the meter key for typos, or create a metered price with this meter first (archived prices count too).

A usage event names a meter (e.g. `api.calls`). A meter exists in a brand and mode once a metered price names it (spec §16.7), so usage of a misspelled meter is refused instead of silently going unbilled. `details.meter` is the key the event used.

### `usage_occurred_at_out_of_range`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The event's occurredAt is too far in the future or the past. Send when the usage happened: at most 5 minutes ahead of now (clock skew) and at most three years back.

`details.reason` is `future` or `too_old`. Events from a period that was already invoiced are accepted and billed on the next invoice as late usage (spec §16.7).

### `validation_failed`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The request is invalid. Fix the fields listed in `details.issues` and send the request again.

### `waitlist_confirmation_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This waitlist confirmation link isn't valid. Links work for 7 days and only the latest one sent works. Join the waitlist again to get a new email.

### `waitlist_consent_required`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The customer hasn't agreed to be notified. Ask the customer to tick the consent (`Notify me when … can sell in …`) before joining.

### `waitlist_link_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This waitlist link isn't valid. Use the link exactly as it is in the email. The checkout link works only after the email saying the brand can sell in your country.

### `wallet_currency_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The amount's currency doesn't match the wallet's currency. Use the wallet for this currency. A customer has one wallet per currency (`details.expected`, `details.actual`).

### `wallet_payment_method_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This payment method can't be used for the wallet's auto-reload. Use a saved payment method (`pm_…`) of the wallet's customer that isn't detached. Customers save one at a subscription or `setup` checkout.

### `wallet_posting_mismatch`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

The posting doesn't credit the wallet with exactly the granted amount. Credit the grant's amount to the wallet account its source belongs to (`wallet:promo` for promo grants, `wallet:available` for the others) and don't move any other wallet account.

A credit grant and its posting must move the wallet by the same amount, so the wallet's balance always equals the remaining amount of its grants (spec §11.5, invariant 4). `details.account` names the wallet account that is off, with `details.expected` and `details.actual` in integer micros.

### `webhook_endpoint_limit_reached`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This brand already has the most webhook endpoints allowed in this mode. Delete an endpoint you no longer use, or subscribe an existing endpoint to more event types.

A brand can have at most 16 webhook endpoints per mode. `details.limit` is the limit.

### `webhook_event_type_invalid`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

One or more event types can't be delivered to this endpoint. List event types from the event catalogue that are delivered to webhooks (docs: reference → events), or "\*" for all of them.

Brand endpoints receive the brand's events that the catalogue marks as delivered to webhooks.
Organization and entity events aren't delivered to brand endpoints, and `webhook_endpoint.test` is sent
only by the test action. `details.problems[]` has `type`, `reason` (`unknown` or `not_deliverable`) and
`message`.

### `webhook_url_not_allowed`

**HTTP status:** 400 · **Type:** `invalid_request` · **Retryable:** no

This URL can't receive webhooks. Use a public https URL without a user name, password or #fragment, e.g. <https://example.com/webhooks>. In development, <http://localhost> works while the dev gate is open.

Webhooks POST account data, so Ownrecord only calls public https URLs: never localhost, private networks
(10/8, 172.16/12, 192.168/16, fc00::/7, single-label or `.local`/`.internal` names), link-local addresses
(169.254/16, fe80::/10) or reserved ranges. Live mode is always https. In development and test, while the
dev gate is open, loopback and private hosts are allowed, and `http` for loopback hosts. The same rule is
checked again for every address the host resolves to when a delivery is made; a refused address fails the
attempt. `details.reason` is `invalid`, `too_long`, `scheme`, `https_required`, `credentials`, `fragment`
or `host_not_allowed`.

## Authentication

### `account_disabled`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

This account is disabled. Contact your organization's owner or Ownrecord support.

A disabled user can't sign in, and every session they had ends immediately.

### `api_key_invalid`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

The API key is invalid, expired or revoked. Send a current key as `Authorization: Bearer or_test_…` or `or_live_…`. Create a new key in the console under Developers → API keys if this one was revoked or has expired.

Returned when the presented key isn't an Ownrecord API key, doesn't exist, was revoked, has expired, or
belongs to an organization that can't use the API in that mode (closed, or suspended for live keys).
The response never says which, so a leaked or guessed key reveals nothing. Request logs show only the
key's 12-character prefix.

### `assistant_session_required`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

The assistant works only in the console, signed in with your own account. Open the console in your browser and sign in. API keys, OAuth access tokens and MCP clients can't use the assistant; they call the operations directly.

`POST /api/assistant/chat` and `POST /api/assistant/actions/confirm` accept only the console's session
cookie, from a page of the same origin. A request with an `Authorization` header (an API key or an OAuth
token) is refused, so the assistant always acts as the person at the keyboard (spec §43.3).

### `authentication_required`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

Authentication is required. Send a valid API key or access token in the Authorization header.

### `dpop_proof_invalid`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

The DPoP proof is missing, invalid, already used, or doesn't match this request. Make a new proof for every request: a JWT with `typ: dpop+jwt`, signed with ES256 or EdDSA by the token's key, with its public `jwk` in the header and the claims `jti` (new each time), `htm` (the HTTP method), `htu` (the URL without query), `iat` (now) and `ath` (the base64url SHA-256 of the access token).

Returned for `Authorization: DPoP or_at_…` when the `DPoP` header is missing or repeated, isn't a proof JWT,
is signed with another algorithm or key, names another method or URL, is more than 60 seconds old or ahead
of the server's time, has an `ath` for another token, or was already used (each `jti` is accepted once).
The message says which check failed. The response carries
`WWW-Authenticate: DPoP error="invalid_dpop_proof"` with the accepted algorithms.

### `dpop_proof_required`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

This access token is bound to a DPoP key: send it with the DPoP scheme and a DPoP proof. Send `Authorization: DPoP <access token>` and a `DPoP` header holding a new proof, signed with the key the token was issued for, on every request. A token bound to a key never works as a bearer token.

The client asked for the token with a DPoP proof, so Ownrecord bound it to that proof's key (`token_type:
"DPoP"` in the token response). Presented as `Authorization: Bearer`, it is refused (RFC 9449 §7.2): whoever
copied the token doesn't hold the key. The response carries
`WWW-Authenticate: DPoP error="invalid_token"` with the accepted algorithms.

### `email_unverified`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

Your identity provider hasn't verified this email address. Verify the address with Google or Microsoft first, or sign in with a code by email instead.

### `oauth_token_invalid`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

The access token is invalid, expired or revoked. Get a new access token from the Ownrecord OAuth server (`/oauth/token`): with your agent's client credentials, or with your refresh token for an installed app or an MCP client. If that fails, the agent or app was paused or revoked in the console (Organization → Agents & apps).

Returned for an `Authorization: Bearer or_at_…` token that doesn't exist, has expired (access tokens last 15
minutes), was revoked, or belongs to an agent, app installation or client that is no longer active, or to a
user who lost access to the organization. The response never says which: request logs keep the reason.
OAuth errors carry `WWW-Authenticate: Bearer` with `error="invalid_token"` on the MCP server.
With `Authorization: DPoP or_at_…` it is also returned for a token bound to another key than the proof's, or
for a bearer token (one issued without a proof); those answers carry `WWW-Authenticate: DPoP error="invalid_token"`.

### `portal_link_invalid`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

This sign-in link doesn't work any more. Ask for a new sign-in link on the portal's sign-in page. Links work once, for 15 minutes; an operator can also generate a new one.

A customer portal sign-in link is single-use and expires 15 minutes after it was made (spec §29.2). The same answer is given for a link that never existed, one that was already used, one that expired and one made for another brand, so a link can't be probed.

### `staff_sign_in_method`

**HTTP status:** 401 · **Type:** `authentication` · **Retryable:** no

Platform staff sign in with Google or Microsoft, not with a code by email. Use Continue with Google or Continue with Microsoft.

## Permission

### `agent_delegation_not_allowed`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

You can't give an agent more access than you have yourself. Choose a narrower role, scope or token scopes, turn off live mode for the agent, or ask an organization owner to do it.

An agent's role at its scope, its token scopes and live access are each checked against what the caller may do
there (spec §41.3). `details.problems[]` lists the permissions the caller doesn't hold.

### `api_key_scope_not_allowed`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

An API key can't have permissions that you don't have yourself. Remove the scopes listed in `details.problems`, or ask an organization owner or admin to create the key.

A key is never broader than its creator: every scope is checked against what the creator may do at the
key's organization, entity or brand, in the key's mode. For example, a developer can't create a key with
`members:admin`, and a developer without live access can't create live keys.
`details.problems[]` has `scope`, `code` (`permission_denied` or `livemode_not_allowed`) and `reason`.

### `app_installation_paused`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

This app is paused in the organization. An organization owner or admin can resume it under Organization → Agents & apps. If the member who installed it left, an owner must re-assign it first; calls then resume with the same tokens.

An admin paused the installation (its tokens were revoked; authorize again after it is resumed), or the
member who installed it lost access to the organization (spec §41.3 "Installer leaves the org"): the
installation is paused and the owners are asked to re-assign it.

### `app_scopes_not_approved`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

Some of these scopes aren't approved for this listed app yet. Consent only to the scopes platform review approved (`details.approved`). The others wait for the publisher's re-review; they can be added after it is approved.

Another organization can grant a listed app only the scopes its approved listing covers. While the app's added
scopes are under review, existing installations keep the scopes they consented to and new consents are limited
to the approved ones (docs/spec-questions.md #1022).

### `approval_policy_denied`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

The organization's approval policy doesn't let agents and apps run this kind of operation. Nothing ran. The policy denies this risk tier (`details.tier`) to agents, apps and MCP clients. Ask an organization owner or admin to change it in Settings → Approvals, or have a person do it in the console.

Approval policies set, per risk tier, whether agents, apps and MCP clients may run an operation (`allow`),
need a person's approval (`require_approval`) or may not run it at all (`deny`). The strictest setting of
the organization's policy and its overrides for the entity, agent or app wins. `details` has the `tier` and
the `operationId`. A dry run of the call fails the same way.

### `approver_not_eligible`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

You can't decide on this approval request. Approval requests are decided by people in the Ownrecord console (not API keys, agents, apps or MCP clients) who may run the operation themselves, at its entity or brand and in its mode. Ask such a person.

### `assistant_disabled`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

Your organization turned the assistant off. Ask an organization owner or admin to turn it on in Organization → Security → Assistant. Everything else in the console keeps working.

### `fresh_auth_required`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

This action needs a recent sign-in. Sign in again, then repeat the action within 15 minutes.

### `invitation_email_mismatch`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

This invitation is for a different email address than the one you're signed in with. Sign out, then sign in with the address the invitation was sent to. Or ask for an invitation to your own address.

### `livemode_not_allowed`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

You can't do this in this mode (test or live). Use test mode, or credentials for the target's mode. Developers need live access granted by an organization owner or admin.

Returned when the caller is allowed the action in principle but not in the requested mode:

- an API key used on data of the other mode (a test key on live data, or the reverse);
- a member whose live access is off (developers start in test mode until an owner or admin grants it);
- a developer changing products and prices in live mode (developers manage the catalog in test mode only).

### `org_not_granted`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

This access token doesn't cover that organization. Use an `Ownrecord-Org` the token was granted for, or ask a member of that organization to install the app or connect the client there.

### `owner_required`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

Only an owner of the organization can do this. Ask an organization owner (`org_owner`).

### `permission_denied`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

You don't have permission to do this. Ask an organization owner or admin for a role or scope that allows this action.

### `portal_action_not_allowed`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

This can't be done in the customer portal. The brand's portal settings don't allow it for this subscription now (`details.action`): contact the brand.

A customer portal action the brand's portal settings or the subscription's state don't allow (spec §16.12, §29.2): a plan switch without a row in the plan-change matrix or with plan changes off, a quantity change with quantity changes off, a cancellation with cancelling off, any change while a cancellation is scheduled or a change waits for its payment, reactivating a cancellation the brand scheduled, or a payment method on a subscription paid by bank transfer. `details.action` names the action: `switch_plan`, `change_quantity`, `apply_promotion_code`, `cancel`, `reactivate` or `change_payment_method`.

### `refund_limit_exceeded`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

This refund is above the support role's limit. Ask a member with the finance role (or an API key with `refunds:admin`) to make it, or refund at most `details.limit`. The limit is the brand's (`brands.policies.supportRefundLimits`).

Support refunds up to a limit, finance above it (spec §3.1). The limit is per brand and currency (`policies.supportRefundLimits` on the brand); a currency without one has the default of 100 major units (100.00 EUR, 100 JPY). The credit note's total is compared with it. `details.limit` and `details.amount` are integer micros.

### `refund_outside_policy`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

Refunding these credits is outside the brand's refund policy. Credits are refundable by support within the brand's refund window when none were used; anything else is the finance role's decision (`refunds:admin`).

Purchased, unused credits are refundable per brand policy: by default within 14 days of the purchase and unused, or at the operator's discretion (spec §17.7). Ownrecord reads the discretion as the finance role's: support refunds credits only inside the policy (`details.reason`: `window` or `used`).

### `role_not_grantable`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

You can't grant or change access that goes beyond your own. Ask an organization owner to grant this role and scope, or pick a role and scope within your own access.

Nobody grants a role, scope or live-mode access that gives more than they have themselves, and nobody changes or removes a membership they couldn't have granted (for example, an admin can't remove an owner). `details.problems` lists what's missing.

### `scope_mismatch`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

The request names an organization, entity or brand outside the scope of your credentials. Use credentials scoped to that organization, entity or brand, or leave the scope fields out of the request.

Every request runs in one scope: an organization, optionally narrowed to one legal entity or brand. Filters and new objects may narrow that scope but never name another organization, entity or brand. Such requests are refused rather than silently rewritten.

### `self_approval_not_allowed`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

You can't approve your own request. Ask another person who may run this operation to approve it.

### `signup_closed`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** yes

New accounts can't be created right now. Try again later. If you already have an account, sign in with the address you used before.

### `support_access_disabled`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

This organization doesn't allow support access. Help without access, or ask an organization owner to change the support-access policy in Organization → Security → Access log.

The organization's support-access policy is `off`: Ownrecord staff can't request access to its console. Only
an organization owner can change the policy (`support_access.update_policy`). Under `on_request` (the default)
staff request access and an owner or admin decides; under `always_read` read access is granted without approval
and still logged.

### `task_claim_not_allowed`

**HTTP status:** 403 · **Type:** `permission` · **Retryable:** no

This credential can't hold tasks. People, organization agents and installed apps claim tasks. With an API key, work on tasks nobody holds without claiming them, or use an agent or app credential (OAuth).

## Entitlement

### `assistant_message_limit_reached`

**HTTP status:** 403 · **Type:** `entitlement` · **Retryable:** no

Your organization has used all of this month's assistant messages. Upgrade the organization's plan for more messages (`details.limit` is this plan's monthly limit), or wait until the first day of next month (UTC).

Each plan includes a number of assistant messages per organization and calendar month (UTC), the
`assistant.messages_per_month` entitlement. Every message you send counts once, however many tools the
assistant uses to answer it. `details` carries `limit`, `used` and `month`.

### `entitlement_required`

**HTTP status:** 403 · **Type:** `entitlement` · **Retryable:** no

Your plan doesn't include this. Upgrade the organization's plan, or remove something to get back under the limit.

### `org_restricted`

**HTTP status:** 403 · **Type:** `entitlement` · **Retryable:** no

Your organization is restricted because its Ownrecord subscription is unpaid, so nothing new can be created in live mode. Pay the open Ownrecord invoice in Organization → Billing; the restriction lifts as soon as it is paid. Test mode keeps working.

Spec §37.5: 14 days (by default) after a failed Ownrecord payment the organization is `restricted`. The console works, existing checkouts, renewals, usage billing and webhooks keep running, but no new live brands, products, prices or API keys can be created. `details.action` names what was refused.

### `org_suspended`

**HTTP status:** 403 · **Type:** `entitlement` · **Retryable:** no

This seller is unavailable: its Ownrecord account is suspended, so it can't start new live sales. If this is your organization, pay the open Ownrecord invoice in Organization → Billing; it is active again as soon as it is paid. Renewals, refunds, the customer portal and filings keep working.

Spec §37.5: 30 days (by default) after a failed Ownrecord payment the organization is `suspended`. New live checkout sessions and new live subscriptions are refused (the message is safe to show a buyer: the seller is unavailable), on top of the restricted organization's limits. Renewals of existing subscriptions, refunds, portal access and filings continue, so the subscriber can meet its obligations to its customers and tax authorities. A platform admin can also suspend an organization for other reasons (§38.4).

## Not found

### `conversation_not_found`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

This conversation doesn't exist, or it isn't yours. Start a new conversation. Conversations are deleted after the organization's retention period, and only their owner can open them.

### `fx_rate_unavailable`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

There's no ECB reference rate for this currency on or shortly before this date. Check the currency (the ECB publishes about 30) and the date. Rates are fetched every day after 16:00 CET; for a date with no rate yet, try again after the next publication.

A conversion uses the ECB rate of the date, or of the last publication day before it (weekends and TARGET holidays have no rates). If the last rate is more than 7 days older than the date, the conversion is refused rather than using a stale rate. `details.date` is the requested date, with `details.latest_rate_date` (the last earlier rates, if any) or `details.currency` (a currency the ECB doesn't publish).

### `invitation_invalid`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

This invitation link isn't valid. Check that you opened the whole link from the invitation email, or ask the person who invited you to send a new invitation.

### `invoice_upcoming_none`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

This subscription has no upcoming invoice. Nothing is billed at the next period end: the subscription has ended, is set to end without anything left to bill, is paused without invoices, or is unpaid. Read the subscription for its status.

`details.reason` is `ended`, `nothing_to_bill`, `paused` or `unpaid`.

### `operation_not_found`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

There is no operation with this ID. Check the operation ID against the API reference. Operation IDs are dotted, e.g. `brands.create`.

### `ownership_transfer_not_found`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

There's no open offer of ownership for you in this organization. Offers expire after 7 days. Ask an owner to offer ownership again.

### `proposal_not_found`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

This proposed action doesn't exist in the conversation. Ask the assistant to propose the action again.

### `resource_not_found`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

The requested object does not exist or is not visible to you. Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.

### `route_not_found`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

There is no API endpoint at this path. Check the method and path against the API reference. Every path starts with /api/v1.

### `session_not_found`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

This session doesn't exist or has already ended. Reload your account page to see the sessions that are still active.

### `subscription_item_not_found`

**HTTP status:** 404 · **Type:** `not_found` · **Retryable:** no

This subscription has no item with this ID. Read the subscription and use one of its `items[].id` (`si_…`); to add a new price, leave out `id`.

## Conflict

### `agent_revoked`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This agent was revoked and can't be changed. Create a new agent instead.

### `alert_resolved`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This alert is already resolved. Nothing to acknowledge: the alert is closed. If the problem happens again, a new alert is raised.

### `already_exists`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

An object with the same unique value already exists. Use a different value, or fetch the existing object and update it.

### `api_key_revoked`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This API key was revoked and can't be changed. Create a new key instead.

### `app_listing_not_withdrawable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This app has no application waiting for review or for changes. Only an application that is `pending` or `changes_requested` can be withdrawn. Read the listing with `apps.listing.get`.

### `app_review_not_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This app isn't waiting for review. Reload the review queue: the application was already decided or withdrawn, or the partner hasn't submitted its changes yet.

### `app_suspended`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Ownrecord suspended this app. The app can't be installed, resumed or authorized while the suspension lasts. Its organization's owners and admins, and those of every organization that installed it, were emailed the reason. Once Ownrecord reinstates it, resume its installation under Organization → Agents & apps.

Platform staff suspend an abusive or compromised app everywhere (`platform.apps.suspend`): its tokens are
revoked in every organization, its installations are paused, the agents it owns stop, and the OAuth server issues
it nothing. Installing it, resuming one of its installations or consenting to it then fails with this error.
Reinstating it (`platform.apps.reinstate`) doesn't resume any installation: each organization resumes its own
with `app_installations.resume`, after which the app asks for authorization again.

### `approval_expired`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The approval request expired before anyone approved it, so it didn't run. Send the request again with a new Idempotency-Key to create a new approval request. Requests expire after 72 hours.

### `approval_not_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This approval request was already decided. Read the request (`GET /approvals/{id}`) to see its status and outcome.

### `approval_policy_override_limit_reached`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This organization already has the maximum number of approval policy overrides. Delete an override you no longer need (`details.limit` is the maximum), or set the rule in the organization's policy instead.

### `approval_rejected`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

An approver rejected this request, so it didn't run. Read the reason in `details.approval.rejection`. To try again, send a new request with a new Idempotency-Key.

### `aup_report_already_resolved`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This AUP report is already resolved. Reload the organization's page. Record a new report if the problem came back.

### `authorization_status_invalid`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This authorization is revoked. A revoked authorization doesn't change. Record a new one with `authorizations.create`.

### `bank_statement_already_imported`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This bank statement file was imported already. Open the earlier import (`details.import_id`) instead. Overlapping statements with different files are fine: their common lines are skipped.

### `bank_statement_line_resolved`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This bank statement line was matched or ignored already. Read the line: `matchStatus` and `invoiceId` say how it was resolved. A matched line paid its invoice; reverse that with the invoice's own actions if it was wrong.

### `brand_not_live`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This brand isn't live yet, so it can't take live payments. Use test mode, or complete the brand's go-live checklist first.

Live checkout sessions (and live API keys) need a live brand (spec §38.3).

### `checkout_details_required`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The customer's details aren't in yet. Submit the customer's email and country first, so the tax can be quoted.

### `checkout_fulfilment_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The paid checkout isn't ready to be fulfilled yet. Nothing to do: the payment is fulfilled automatically as soon as the checkout's records are committed. If it stays pending, contact support with the payment's ID.

Fulfilment runs on the processor's webhook (spec §14.1 step 8). A webhook can arrive before the request that prepared the payment has committed; the job then retries with backoff.

### `checkout_payment_method_required`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Choose a payment method first. Start the payment step and choose the card, then confirm. The card's country can change the tax, and subscriptions save it for renewals.

### `checkout_payment_not_started`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The payment step hasn't started for this checkout. Start the payment step first, so the payment can be prepared for the quoted total.

### `checkout_quote_expired`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The tax quote of this checkout has expired. Submit the customer's details again to get a fresh quote (quotes are valid for 30 minutes).

### `checkout_session_blocked`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This sale is paused for the customer's location, so it can't be paid. Offer the waitlist, or let a business customer enter a valid tax ID (reverse charge). `details.blocked_reason` says why the sale is paused.

### `checkout_session_changed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The checkout changed while this request ran. Send the request again.

### `checkout_session_not_open`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This checkout can't be changed any more. The session is complete, expired or waiting for a payment's outcome (`details.status`). Create a new session for a new purchase.

### `checkout_total_changed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The total changed since the customer saw it. Show the customer the new total (`details.total`) and ask them to confirm it; then confirm again with that total.

The card's country is location evidence (spec §13.3). When it changes the tax, the total changes, and the customer must confirm the new total before they are charged. `details.total` is the amount to show.

### `coupon_immutable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This coupon has been used, so what it gives can't change. Create a new coupon with the new terms (and new promotion codes for it), then archive this one. Its name and status can still change.

Once a discount references a coupon or a redemption is counted, its amount or percentage, duration, product and price restriction, `maxRedemptions` and `redeemBy` are fixed, so every customer keeps what they were promised. `details.fields` lists the fields the request tried to change.

### `coupon_not_redeemable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This coupon can't be redeemed. Check `details.reason`: reactivate an archived coupon, or create a new coupon when it expired or reached its redemption limit.

`details.reason` is `archived` (the coupon is archived), `expired` (its `redeemBy` has passed), `max_redemptions_reached` (its `maxRedemptions` are used up) or `currency_mismatch` (an amount-off coupon in another currency than the purchase). Discounts already applied keep running; only new redemptions are refused.

### `credit_note_pdf_not_ready`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The credit note PDF isn't ready yet. The PDF is rendered right after the credit note is issued, usually within a minute. Try again shortly; `pdf.status` on the credit note says when it's ready.

### `customer_email_taken`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Another customer of this brand already has this email address. Use the existing customer (`details.customer_id`, or `customers.list` with `email`), or give this customer another address. A deleted customer's address can be used again.

A brand has one active customer per email address in each mode, because checkout finds returning customers and the customer portal signs them in by email. The same address in another brand, or in the other mode, is a different customer (spec §6.6).

### `customer_tax_id_exists`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The customer already has this tax ID. Nothing to do: the tax ID is already on the customer (`details.tax_id_id`). To check it again, remove it and add it back.

Tax IDs are compared by their canonical number, so `ATU12345678`, `atu 1234 5678` and `12345678` for Austria are the same ID, as are a Swiss `ch_uid` and `ch_vat` of one UID.

### `discount_already_applied`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This coupon is already applied here. End the existing discount first (`discounts.delete`), or apply a different coupon. The same coupon doesn't stack with itself.

A subscription (or a customer, for customer-level discounts) can hold a coupon once at a time: applying it again while its discount is active or scheduled would discount twice. `details.discount_id` is the existing discount.

### `dispute_evidence_changed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The evidence pack changed while it was being submitted. Reload the dispute, check the latest pack and submit again.

Two people built or submitted the pack at the same time. Nothing was recorded for this request; the processor may already have the evidence of the other one (`under_review`).

### `dispute_evidence_missing`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Build the evidence pack before submitting it. Build the pack with `disputes.evidence.build`, check it in the console (or in the returned `evidence.sections`), then submit it.

### `dispute_evidence_past_due`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The deadline to respond to this dispute has passed. The processor no longer takes evidence after `evidenceDueBy`. The dispute closes as the card network decides; Ownrecord records the outcome from the processor's webhook.

### `dispute_payment_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The disputed payment isn't recorded yet. Nothing to do: the processor-event job retries shortly, once the payment's own webhook has been handled. If the payment never appears, a RECON_UNMATCHED alert follows.

Raised by the processor-event job, never by an API call: a dispute arrived before the payment it disputes was recorded (the processor delivered both at once).

### `dispute_status_invalid`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This dispute can't take that change in its current status. Reload the dispute. Evidence is built and submitted only while the dispute needs a response (`needs_response`); once submitted (`under_review`) or closed (`won`, `lost`) it can't change. `details.status` is the dispute's status.

### `domain_purpose_taken`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This brand already has a domain for this purpose. Remove the brand's current domain for this purpose first, then add the new one.

A brand has at most one checkout domain and one portal domain. `details.host` is the one it has.

### `domain_taken`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This domain is already attached to a brand. Use another domain, or remove it from the brand that uses it first.

### `domain_unavailable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This domain is in use elsewhere on the hosting platform and can't be attached. Remove the domain from the other project or account that uses it, or use another host name, then try again.

The domain provider (Vercel) refused to attach the host because another project or account holds it.

### `einvoice_not_retryable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This e-invoice can't be retried. Only a `failed` or `rejected` e-invoice can be sent again. One that is pending, queued, delivered or accepted is on its way or done.

`details.status` is the e-invoice's current status.

### `entitlement_override_limit`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This customer already has the most active entitlement overrides allowed. Delete overrides the customer no longer needs (`customers.entitlement_overrides.delete`), or let expiring ones run out, then try again.

A customer can have at most 100 active (unexpired) overrides; expired ones don't count. `details.limit` is the maximum.

### `entity_already_verified`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This legal entity is already verified. Nothing to do. Change its tax ID to verify it again.

### `entity_verification_not_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This legal entity isn't waiting for document review. Reload the verification queue: the entity was already approved or rejected, or it was verified another way (VIES, HMRC, CH UID).

### `export_expired`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This export has expired. Export files are kept for 30 days after they are ready. Request the same period again.

### `export_not_ready`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

This export's files aren't ready. An export is `queued`, then `running`, then `ready` (usually within a minute); poll `exports.get` until it is `ready`. A `failed` export says why in `failure`: request a new one.

### `feature_change_not_allowed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

A feature's type can't change, and enum values can be added but not removed. Create a new feature with the new definition, and grant it from your products instead.

Products and customers may already hold values of the feature, so changes that could make those values invalid are refused. `details.issues` names each refused change.

### `feature_key_taken`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This brand already has a feature with this key in this mode. Choose another key, or update the existing feature.

### `filing_period_not_ended`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This filing period isn't over yet, so it can't be locked. Lock the period from `details.lockable_at` on: after its last day (plus the lock delay for late documents).

A period is locked after it ends (spec §21.1), so every document of the period is in the report. `details` has `period_end` and `lockable_at`.

### `filing_period_status_invalid`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This filing period can't do that in its current status. Reload the period and check its `status`: `details.allowed_from` lists the statuses the action needs. Periods go open → locked → prepared → submitted → paid.

Filing periods follow spec §21.1: lock an `open` period once it is over; prepare (generate the report) a `locked` or `prepared` one; submit a `prepared` one; mark a `submitted` one paid. `details` has `status`, `action` and `allowed_from`.

### `filing_reconciliation_failed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The report's tax doesn't match the ledger's tax payable for the period. Check the documents in `details.documents`: each shows the report's and the ledger's tax. Correct the ledger or the documents (for example with a manual ledger adjustment or a credit note), then prepare the period again.

Every report has a reconciliation section (spec §21.2): its tax per jurisdiction and currency must equal the movement of the regime's tax payable accounts in the ledger for the period. When they differ, no report is generated. `details.rows` lists every jurisdiction and currency that differs (`report_tax`, `ledger_tax`, `difference`, micros as strings), `details.documents` the invoices, credit notes and ledger transactions behind the difference.

### `filing_registration_not_effective`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The registration this period files for has no effective date yet. Mark the registration registered with its number and effective date first: sales made during the grace period are reported in the first return after it.

Periods are created for regimes in grace too, so their due dates show on the calendar, but a return can only be prepared once the registration has an effective date: that date decides which return reports the grace sales (spec §21.1). `details` has `regime_id` and `registration_state`.

### `filing_report_kind_unsupported`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Ownrecord can't generate this regime's report yet. Prepare the return outside Ownrecord for now; the report for this kind of regime comes in a later release.

Every regime maps to a report kind (spec §21.2): `eu_oss` for the EU One-Stop Shop schemes, `eu_domestic`, `uk_vat`, `ch_mwst`, `us_state` and `generic`. `details` has `report_kind` and `regime_id`.

### `filing_report_not_ready`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This filing period has no report in that format yet. Prepare the period first (`filing_periods.prepare`), then download its report.

Reports are generated when a period is prepared (spec §21.2) and kept with it as CSV and XLSX files. `details` has `status` and `format`.

### `filing_schedule_conflict`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The new filing schedule would cut an existing filing period. Choose a frequency and stagger whose periods start where the first existing period starts and end where the last one ends (`details.period`, `details.edge`, `details.at`).

Filing periods never overlap and leave no gap. A schedule may change after periods exist only when the existing periods stay whole: their first start and their last end must be period boundaries of the new schedule. `details` has `regime_id`, `period` (the period cut), `edge` (`start` or `end`) and `at`.

### `go_live_requirements_unmet`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The brand can't go live yet: some go-live requirements aren't met. Open the brand's go-live checklist (`brands.go_live_checklist`) and complete the open items.

`details.unmet` lists the requirements still open (`plan_livemode`, `org_status`, `legal_accepted`, `entity_verified`, `invoice_settings`, `home_registration`, `processor_live`, `brand_urls`, `brand_status`) and `details.items` explains each one.

### `idempotency_in_progress`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

A request with this Idempotency-Key is still being processed. Wait briefly and retry with the same Idempotency-Key.

### `idempotency_key_reused`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This Idempotency-Key was already used with a different request. Use a new Idempotency-Key for a different request, or resend the original request unchanged.

### `invitation_already_accepted`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This invitation was already accepted. Open the console to see your organizations. Each invitation works once.

### `invitation_exists`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This person already has a pending invitation at this scope. Ask them to use the invitation they have, or revoke it and invite them again.

### `invitation_expired`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This invitation has expired. Invitations work for 7 days. Ask the person who invited you to send a new invitation.

### `invitation_not_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Only pending invitations can be revoked. This invitation was already accepted, revoked or has expired. Nothing to do.

### `invitation_revoked`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This invitation was revoked. Ask the person who invited you to send a new invitation.

### `invoice_bank_transfer_missing`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The legal entity has no bank transfer details for this invoice. Add the IBAN, BIC, bank name and account holder in the entity's invoice settings, then send the invoice again. A send_invoice invoice tells the customer where to pay.

### `invoice_item_attached`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This invoice item is already on another invoice. An item is billed once. Remove it from its draft first (`details.invoice_id`), or create a new item.

### `invoice_payment_unavailable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This invoice can't be paid by card now. The invoice is paid, voided or has nothing due, or the brand has no processor connection: check `details.reason`. Pay by bank transfer with the details on the invoice, or ask the seller.

`details.reason` is `not_open` (paid, voided or a draft), `nothing_due`, `no_processor` (the brand has no usable processor connection) or `payment_pending` (a card payment of the invoice is being processed).

### `invoice_pdf_not_ready`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The invoice PDF isn't ready yet. The PDF is rendered right after the invoice is issued, usually within a minute. Try again shortly; `pdf.status` on the invoice says when it's ready.

### `invoice_quote_unusable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This tax quote can't be invoiced. Invoice the quote the customer paid: a quote that isn't blocked, whose lines add up to its totals. Create a new quote if the sale changed.

An invoice shows exactly the tax of the quote the customer paid (spec §9.7, §18). A blocked quote, or a line the engine blocked, never gets an invoice (fail closed, §1.3).

### `invoice_series_taken`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Another brand of this legal entity already uses this invoice series. Choose a different series code. Each brand numbers its invoices and credit notes in its own series within the entity.

Invoice and credit-note series are unique across all brands of one legal entity (spec §18.2), so every invoice number is unique and continuous. `details.series` names the clashing code.

### `invoice_status_invalid`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The invoice's status doesn't allow this. Check the invoice's `status` (`details.status`) and what it still allows (`details.allowed_actions`). Drafts are edited and finalized; open invoices are sent, paid, voided or marked uncollectible; paid and void invoices don't change (corrections are credit notes).

Invoices move `draft → open → paid | void | uncollectible` (an uncollectible invoice can still be paid). Only a draft's lines and settings change; once finalized an invoice's number, lines and totals are fixed. Invoices issued paid at checkout (`issued`, `pending_registration`) allow none of these actions.

### `invoice_total_zero`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

An invoice that totals zero isn't finalized. Add an item with an amount, or void the draft. Zero invoices aren't issued (spec §16.2): no number is taken for them.

`details.total` is the invoice's gross total in integer micros (zero or negative). A draft whose credits exceed its charges isn't issued either: the credit belongs on the customer's credit balance.

### `knowledge_review_item_transition_invalid`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This review item can't do that in its current status. Read the item (`GET /tax/knowledge/review_items/{id}`): resolve or dismiss an open item, reopen a resolved or dismissed one, and assign only open items.

Review items are `open`, `resolved` or `dismissed` (spec §7.2). Resolving needs an open item and the commit or pull request that made the change; dismissing needs an open item and a reason; reopening needs a resolved or dismissed item. `details` has the item's `status` and the `requested` one.

### `last_owner`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

An organization needs at least one owner, and this is the last one. Make another member an owner first, then change or remove this membership.

### `lease_closed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The lease is already closed. Settle the lease it was renewed into (`details.renewedTo`), or create a new lease. A lease closes with a `final` settle or a renewal.

Each lease ID is settled until a settle closes it: `final: true` captures the consumed amount and releases the rest, and `renew` replaces it with a new lease ID. A resent copy of the closing settle is answered as a duplicate; any other settle of the closed lease gets this error.

### `lease_expired`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The lease has expired, so it can't be settled. Create a new lease for further usage. To settle usage after a crash, make leases with the expiry policy `hold_for_late_settle`, which accepts a settle until `lateSettleUntil`.

A lease can be settled until its `expiresAt` (spec §17.3). After that its expiry policy applies, whether or not the `lease_expiry` job has run yet: `release` returns the funds to the wallet, `capture` charges the whole remaining amount, and `hold_for_late_settle` keeps the funds reserved and accepts one closing settle until `lateSettleUntil` (`expiresAt` plus `lateSettleWindowSeconds`). `details.expiresAt` and, for held leases, `details.lateSettleUntil` say when the lease ran out.

### `lease_settle_out_of_order`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

An earlier settle of this lease hasn't been applied yet. Send the settles of a lease in order, one at a time: `settleSeq` must be the lease's `settleSeq` plus one (`details.expected`). Retry once the earlier settle has been applied.

`settleSeq` is optional. When a settle carries it, Ownrecord applies settle n only after settle n − 1, so a `final` settle that overtakes an earlier one can't close the lease before the earlier usage is captured. A settle numbered at or below the lease's `settleSeq` was applied already and is answered as a duplicate.

### `lease_settle_seq_conflict`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

A settle with this number was already applied with different content. Give each settle of a lease the next number (`details.settleSeq` is the lease's current one) and resend a settle only unchanged.

### `ledger_rebuild_refused`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This ledger account can't be rebuilt from its entries. An entry in another currency or an unbalanced transaction touches the account (`details.problems`): that is a different break (invariant 1). Follow docs/runbooks/ledger-invariant-break.md before rebuilding.

### `ledger_transaction_not_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Only a pending ledger transaction can be posted or voided. Check the transaction's `status` (`details.status`). A posted transaction is corrected with a reversing transaction, never voided.

Ledger transactions are append-only (spec §11.1). A `pending` transaction (processor-side uncertainty, such as an asynchronous SEPA payment) is either posted or voided once. Posting an already posted transaction again with the same entries, or voiding an already voided one, returns it unchanged.

### `ledger_transaction_not_posted`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Only a posted ledger transaction can be reversed. Void a pending transaction instead of reversing it (`details.status` is its current status).

### `lookup_key_taken`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Another price in this brand and mode already has this lookup key. Move the key explicitly with `prices.move_lookup_key` (POST /prices/{id}/move_lookup_key), or choose another key.

A lookup key names one price per brand and mode (spec §7.4). Moving it from one price to another is an explicit action (spec §12), so setting it on a second price is refused rather than silently moved. `details.price_id` is the price that has it, when it is visible to the request.

### `membership_exists`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This user already has a membership at this scope. Change the role of the existing membership instead of adding another one.

### `meter_definition_conflict`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Another metered price of this meter aggregates or prices it differently. Use the meter's existing aggregation and pricing (and, for percent of amount, its currency), or choose another meter key.

All metered prices of one meter in a brand and mode agree on what the meter is (docs/spec-questions.md #410): the same aggregation (`sum`, `max` or `last`), the same pricing (`per_unit` or `percent_of_amount`) and, for `percent_of_amount`, the same currency, because the quantity is an amount in that currency. `details.field` is what differs, `details.expected` the meter's value and `details.price_id` the price that has it.

### `oauth_client_disabled`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This OAuth client is disabled. Register a new app, or create a new agent.

### `onboarding_step_done`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This onboarding step is already done. Nothing to do. A done step can't be skipped or marked again.

### `organization_already_suspended`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This organization is already suspended. Nothing to do, or reinstate it first to change the reason.

### `organization_closed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This organization is closed. Its data is read-only. You can still read and export its data (`organizations.export`).

### `organization_closing`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This organization is closing and takes no new live checkouts. Existing subscriptions, refunds and filings continue until the organization is closed. Contact the seller for anything else.

### `organization_export_in_progress`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

An export of this organization is already being built. Wait for it to be ready (`organizations.exports.list`), then download it.

### `organization_not_closable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This organization can't be closed from its current status. A suspended organization is closed with Ownrecord support; a closing or closed one needs nothing more.

`details.status` is the organization's status.

### `organization_not_suspended`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This organization isn't suspended for abuse. Only an abuse suspension is reinstated here; a suspension for non-payment ends when the invoices are paid.

### `ownership_transfer_exists`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

An offer of ownership is already open for this organization. Cancel the open offer first (`ownership_transfers.cancel`), then make a new one.

### `ownership_transfer_not_allowed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Ownership can't be offered to this person. Offer ownership to another member of the organization who isn't an owner yet.

`details.problem`: `not_owner` (you aren't an owner), `self`, `not_member` (they have no membership in the organization) or `already_owner`.

### `partner_terms_not_accepted`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The organization hasn't accepted the current Partner and App Terms. An owner or admin reads the Partner and App Terms at /legal/partners and accepts them under Developers → Apps (or with `partner_terms.accept`), then submits the application again.

Listing an app for every organization needs the owning organization's acceptance of the Partner and App Terms
(spec §41.10): the version in effect, or a later one. A new material version in effect asks again before the
next application. `details.status` is the organization's acceptance status and `details.acceptable` the
versions it can accept now.

### `payment_method_detached`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This payment method was detached and can't be used any more. Ask the customer to add a payment method again (a checkout in `setup` mode).

### `platform_billing_details_required`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Ownrecord needs your organization's VAT ID before it can bill a paid plan. Add your legal entity (its VAT ID is used), or enter your billing details and VAT ID in Organization → Billing → billing portal, then change the plan again.

Ownrecord sells to businesses only (§37.6: the organization is a customer of our entity). A paid plan's invoices are business-to-business sales: reverse charged across EU borders, with domestic VAT at home. Before the first one, the organization's billing account needs a VAT ID the register confirms; Ownrecord copies the first legal entity's home tax ID, legal name and address when the billing details have none.

### `platform_billing_unavailable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

Ownrecord's own plans aren't set up on this installation yet. A platform admin runs `pnpm platform:bootstrap`, which creates the plans, prices and features of data/platform/plans.yaml. Try again afterwards.

Platform billing needs the platform brand (§37.1) and its plan catalogue in the billing mode (`platform_settings.defaultPlanPriceId`). Until then organizations keep their plan cache and aren't billed.

### `platform_customer_missing`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

This organization's Ownrecord billing account is still being set up. Wait a few seconds and reload the billing page. If it persists, contact support.

### `platform_last_admin`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The platform needs at least one platform admin. Make someone else a platform admin first, then change this person's role.

### `platform_plan_change_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

A plan change is already waiting for its payment. Finish the checkout you started (its link is on the billing page), or wait until it expires (23 hours), then try again.

### `platform_plan_currency_fixed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Your organization's paid plan is billed in another currency, and a running paid plan keeps its currency. Choose the plan in the currency it is billed in, or move to the free plan first and then choose the plan in the new currency.

### `platform_plan_unchanged`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Your organization is already on this plan and billing interval. Choose another plan or interval, or leave the plan as it is.

### `platform_profile_limit_reached`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This organization has reached the limit of staff notes or AUP reports. Summarise older entries in one note, or ask an engineer to archive the organization's staff profile.

### `price_archived`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This price is archived. Reactivate the price first (`active: true`), or use an active price.

Archived prices can't receive a lookup key or be sold. Reactivating a price makes it usable again.

### `price_immutable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This price has been used, so what it charges can't change. Create a new price with the new terms, move the lookup key to it with `prices.move_lookup_key`, and archive this one.

Once a checkout session or payment references a price, its amounts, currency, tax behaviour, type and tiers are fixed (spec §12), so every invoice and receipt keeps matching it. The nickname, lookup key, metadata and `active` can still change. `details.fields` lists the fields the request tried to change.

### `price_lookup_key_conflict`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This price already has another lookup key. Remove the price's current lookup key first (`lookupKey: null`), or move the key to a price without one.

Moving a lookup key onto a price that has a different one would silently free that other key, which integrators may still use. `details.lookup_key` is the key the price has.

### `processor_account_already_connected`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This processor account is already connected to this entity in this mode. Rotate the key of the existing connection (processor_connections.update) instead of connecting again.

`details.connection_id` is the existing connection. A disconnected connection doesn't count.

### `processor_connection_disconnected`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This processor connection is disconnected. Connect the processor again, or use another connection of the entity.

A disconnected connection's API key is destroyed, so nothing can be charged or refunded through it. Its webhook secret is kept so late events for earlier payments are still recorded.

### `processor_connection_in_use`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Brands still use this processor connection. Assign another connection to the brands listed in `details.brand_ids` first.

### `product_archived`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This product is archived. Reactivate the product first (`active: true`), or use an active product.

New prices can't be added to an archived product, and its prices can't be sold.

### `promotion_code_not_redeemable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This promotion code can't be used here. Check `details.reason` and tell the customer: the code may have expired, be used up, belong to another customer, need a larger purchase or be for first-time customers only.

`details.reason` is `archived`, `expired` (`restrictions.expiresAt` has passed), `max_redemptions_reached`, `customer_mismatch` (the code is for another customer), `first_time_only` (the customer has paid before), `minimum_amount_not_met` (the purchase before discounts is below `restrictions.minimumAmount`, or in another currency) or `coupon_not_redeemable` (the code's coupon can't be redeemed; `details.coupon_reason` says why). Unknown facts fail closed: a code with a minimum needs the purchase amount, a customer's code needs the customer.

### `promotion_code_taken`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This brand already has a promotion code with this code in this mode. Choose another code. Codes are compared without regard to case and are never reused.

Codes are unique per brand and mode, ignoring case (`launch50` and `LAUNCH50` are the same code), and an archived code keeps its code so a customer can't be surprised by a different offer under an old code. `details.promotion_code_id` is the code that has it, when visible to the request.

### `proposal_not_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This proposed action was already confirmed, cancelled or replaced. Ask the assistant to propose the action again if you still want it.

### `reconciliation_item_closed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This reconciliation item is no longer open. Reload the item: it was resolved or ignored already (`details.status`), by a person or by a later reconciliation run that found what it was waiting for. Only `open` items can be resolved or ignored.

### `refund_amount_exceeds`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The refund is larger than what is left to refund. Refund at most `details.refundable`: the payment's amount less earlier refunds, and for credit packs no more than the unused credits are worth.

A refund never returns more than the payment took less earlier refunds, and never credits more than the invoice still has (spec §19.1 step 1). Credit packs and top-ups are refundable only for their unused, unreserved credits; promo and bonus credits never are (§17.7). `details.refundable` is the most this refund type can return now.

### `refund_event_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The processor's refund event can't be matched to a refund yet. Nothing to do: the event is processed again shortly, once the refund is recorded.

### `refund_nothing_to_refund`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

There is nothing left to refund. The invoice is fully credited, its credits are used, or its period is over. Check the invoice's credit notes and the wallet before trying another type.

### `refund_payment_not_refundable`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This payment can't be refunded. Refund a succeeded card payment that has an invoice (`details.status` says what the payment is now). A disputed payment is settled through the dispute; an invoice paid by bank transfer or only from the credit balance has no payment to refund.

Refunds go back through the processor that took the payment (spec §19.1), so they need a payment that succeeded (`succeeded` or `partially_refunded`) and the invoice it paid. `details.reason`: `status` (the payment's status doesn't allow it), `no_invoice`, `no_payment` (the invoice was paid by bank transfer or from the credit balance) or `currency`.

### `registration_grace_expired`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The grace period of the obligation already triggered has passed, so the jurisdiction can't be armed. Complete the registration with the tax authority and mark it registered with its number, effective date and proof.

A threshold crossing starts the registration deadline even while the jurisdiction is blocked. Once that deadline is inside its safety buffer, arming would only start a grace period that ends at once, so it is refused (spec §10.2, §10.3).

### `registration_grace_not_allowed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This regime has no grace period, so the jurisdiction can't be armed. Register with the tax authority first, then mark the registration registered with its number, effective date and proof.

Only regimes that collect tax before registration and have a grace rule (a number of days, a day of the following month or the end of the following month) can be armed. US states never collect before registration, and regimes with the grace rule `none` or `before_first_sale` need the registration first (spec §10.2, §10.3).

### `registration_prerequisites_incomplete`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Every registration prerequisite must be done before the jurisdiction can be armed. Finish the prerequisites on the registration page (portal access, fiscal representative, documents …), mark each one done, then arm again.

Arming tells Ownrecord that the next consumer sale may start the grace period (spec §10.2). Grace needs every prerequisite done, so a registration with open prerequisites can't be armed.

### `registration_transition_invalid`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This registration can't do that in its current state. Check the registration's state (`details.state`): `details.allowed_from` lists the states the action needs. Reload the registration and try the action that fits its state.

Registrations follow the state machine in spec §10.2: arming needs `blocked_b2c` (not armed), marking the application submitted needs `grace` (once), deregistering needs `registered`, and marking registered works from any state except `registered` and `not_applicable`. `details` has `regime_id`, `state`, `armed`, `event` and, for an action no row accepts in that state, `allowed_from`.

### `reservation_expired`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The reservation has expired, so it can't be captured. Reserve the amount again and capture the new reservation. Expired reservations are released automatically.

A single reservation can be captured until its `expiresAt` (at most one hour after it was made, spec §17.2). From then on its funds go back to the wallet, whether or not the `reservation_expiry` job has run yet.

### `reservation_not_open`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The reservation is already closed. Create a new reservation. A reservation is closed once it is captured, released or expired (`details.status`).

### `risk_blocklist_entry_exists`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This value is already on the blocklist. Nothing to do: `details.entryId` is the existing entry. Delete it first to change its reason.

### `risk_review_not_paid`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The held payment hasn't been recorded yet. The review opens when the processor confirms the payment. Wait for its webhook, then decide the review.

### `risk_review_not_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This risk review was already decided. Reload the review: it was approved or rejected (`details.status`). A decision is final; refund the payment or unfreeze the wallet by hand if something must change.

### `slack_channel_limit_reached`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This organization already has the maximum number of Slack channels in this mode. Remove a channel you no longer use (`details.limit` is the maximum), then add the new one.

### `slug_taken`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This slug is already in use. Choose another slug. Organization and brand slugs are unique across Ownrecord; entity slugs are unique within an organization.

### `stale_approval`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The approved request can no longer run as it was requested: the data changed, or the requester lost access. Check the current state (`details.approval.failure` says what failed), then send a new request with a new Idempotency-Key.

An approved request runs exactly as originally requested, as the person or key that requested it, with
their current access. If that no longer works (the requester lost access, a referenced object changed or
a balance moved), the request becomes `stale` and nothing runs (spec §41.4). `details.approval.failure`
has the underlying error code and message.

### `stale_proposal`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The data changed after this action was proposed, so it didn't run. Ask the assistant to propose it again: it shows a fresh preview of what will happen.

Confirm re-runs the proposal's dry run on the current data and runs the action only if the preview is the
same as on the card (spec §43.3). If something changed in between (another person edited the object, a
balance moved, you lost access to the brand), nothing runs and the card becomes `stale`.

### `subscription_meter_conflict`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The customer already has a metered item for this meter. Change the existing subscription's metered item instead, or cancel it first. A customer has at most one active metered item per meter.

Usage is recorded per customer and meter, and billed through a watermark per customer and meter (spec §16.7), so two live subscription items billing the same meter would bill the same usage twice. `details.meter` is the meter and `details.subscription_id` / `details.item_id` the item that already bills it.

### `subscription_status_invalid`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The subscription's status doesn't allow this. Check `details.status`: items change only while trialing, active or past due; pausing needs an active subscription and resuming a paused one; reactivating needs a cancellation scheduled for the period end; canceled and expired subscriptions don't change.

`details.action` is what was asked, `details.status` the subscription's status and `details.reason` why it was refused: `final_status` (canceled or incomplete_expired), `not_allowed` (the §16.1 lifecycle doesn't allow it from this status), `cancel_not_scheduled` (reactivate without a scheduled cancellation) or `cancel_already_scheduled`.

### `support_access_already_open`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

You already have an open support-access request or grant for this organization. Wait for the organization to decide on your request, keep using your current grant, or end it before you request another.

### `support_access_not_active`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This support-access grant isn't active. Reload the Access log: the grant has already ended (expired, revoked or ended by the staff member), or it is still a request waiting for a decision (deny it instead).

### `support_access_not_pending`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This support-access request isn't waiting for a decision. Reload the Access log: the request was already approved or denied, withdrawn, or it lapsed 24 hours after it was made. Staff can make a new request.

### `task_assigned`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This task is assigned to a person, so it can't be claimed. Ask the assignee, or someone who may work on the entity's tasks, to unassign it (`tasks.update` with `assignee: null`), or work on another task.

### `task_claimed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Someone else holds this task's lease. Work on another task (`tasks.list` with `claimable: true`), or try again after `details.claimedUntil`: an expired lease frees the task. `details.assignee` is who holds it.

### `task_input_not_available`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This input isn't available for this task. Read `details.reason`: a registration packet belongs to registration tasks, a filing package to filing tasks once the period is prepared (`filing_periods.prepare`), and a playbook exists only where the tax knowledge has one.

### `task_lease_expired`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

Your lease on this task ended. Claim the task again with `tasks.claim`. If someone else claimed it meanwhile, work on another task. Send heartbeats well before `claimedUntil`.

### `task_not_claimed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

You don't hold this task's lease. Claim the task first with `tasks.claim`; only the holder can extend or release a lease.

### `task_not_ready`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The registration or filing period isn't where this task needs it yet. Read `details`: for example, prepare the filing period (lock it, then `filing_periods.prepare`) before completing `filing.prepare`, which then finishes by itself.

### `task_status_invalid`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This task can't be changed in its current status. Reload the task: finished tasks (`done`, `failed`, `cancelled`) don't change, and a task waiting for an approval changes only through that approval. `details.status` is the task's status.

### `test_clock_advancing`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The test clock is advancing. Wait until the clock's `status` is `ready` (read it with `test_clocks.get`), then try again. An advance processes every due renewal, trial end and cancellation of the clock's subscriptions first.

While a test clock advances (spec §16.13), its subscriptions are billed at each step's time; changing the clock, its customers' subscriptions or attaching new customers would race those steps. `details.test_clock_id` names the clock.

### `usage_idempotency_key_reused`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This idempotency key was already used for a different usage event. Use a new idempotency key for a new event. Resending the original event unchanged is safe: it is reported as a duplicate and counted once.

Every usage event carries its own idempotency key, unique per brand and mode (spec §17.4). An event whose key is already recorded is a duplicate when its customer, meter, quantity, `occurredAt`, attributes and subscription item are the same; otherwise it is refused with this error and nothing is counted.

### `user_already_disabled`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This account is disabled already. Reload the account: its sessions and OAuth access already ended when it was disabled.

### `user_not_disabled`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This account isn't disabled. Reload the account: someone may have enabled it already.

### `version_conflict`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** yes

The object was changed by another request. Fetch the object again and retry the change.

### `waitlist_not_available`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The waitlist is offered only when a sale is paused for the customer's location. The customer can buy now: continue the checkout. The waitlist opens when the quote is blocked (spec §23).

### `wallet_closed`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The wallet is closed. Reopen the wallet with `wallets.update` (`status: active`). A purchase of credits in its currency reopens it, too.

A closed wallet takes no reservations, leases or operator grants (`promo`, `adjustment`). Credits that were paid for (a top-up or a credit pack, subscription credits) are never refused: they reopen the wallet.

### `wallet_frozen`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

The wallet is frozen, so its credits can't be spent. Unfreeze the wallet (`wallets.update` with `status: active`) once the review is done. Reservations made before the freeze can still be captured or released.

Freezing a wallet (for example while a dispute or a risk review is open) stops new reservations and leases at once. Credits still arrive (top-ups, grants, subscription credits), reservations made before the freeze settle as usual, credits still expire, and no auto-reload is started while the wallet is frozen.

### `wallet_not_empty`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

A wallet with credits or open reservations can't be closed. Capture or release its open reservations and wait until its credits are used or expire (`details.balance` in integer micros, `details.open_reservations`). To stop spending at once, freeze it instead.

### `webhook_delivery_in_progress`

**HTTP status:** 409 · **Type:** `conflict` · **Retryable:** no

This delivery is still being attempted. Wait until it succeeds or fails (see `nextAttemptAt`), then redeliver it if you need to.

Only finished deliveries (succeeded, failed or canceled) can be redelivered.

## Rate limited

### `agent_daily_limit_reached`

**HTTP status:** 429 · **Type:** `rate_limited` · **Retryable:** yes

This agent or app has made all the changes it may make today. Try again after midnight UTC (`retry_after_ms`), or ask an organization owner or admin to raise its mutations-per-day limit under Organization → Agents & apps.

An agent or app installation with a `limits.mutationsPerDay` runs at most that many mutating calls per UTC
day and mode. Reads, dry runs, replays of an idempotency key and approval requests don't count; a call that
fails is given back. A dry run answers this error when the real call would. `details` carries `limit`, `used`
and `resets_at`.

### `agent_rate_limited`

**HTTP status:** 429 · **Type:** `rate_limited` · **Retryable:** yes

Too many requests from this agent or app. Wait `retry_after_ms` and try again, and spread your calls out. An organization owner or admin can raise the agent's or app's requests-per-second limit under Organization → Agents & apps.

An agent or app installation with a `limits.rps` makes at most that many operation calls per second (fixed
one-second windows on the real clock, across both modes). Every operation call counts, reads and dry runs too;
the organization's API rate limit (`rate_limited`) applies as well. `details.limit` is the limit per second.

### `assistant_budget_exhausted`

**HTTP status:** 429 · **Type:** `rate_limited` · **Retryable:** yes

The assistant has reached its monthly usage budget. Try again next month, or ask Ownrecord support. Everything else in the console keeps working.

Ownrecord caps what the assistant may spend on its language model each month across all organizations
(spec §43.6). Platform staff are alerted at 80% of the cap; at 100% the assistant answers this refusal until
the next calendar month (UTC).

### `oauth_rate_limited`

**HTTP status:** 429 · **Type:** `rate_limited` · **Retryable:** yes

Too many requests to the authorization server from your network. Wait as long as the `Retry-After` header says, then try again. Reuse access tokens until they expire.

Client registrations and token requests are limited per network address (spec §41.3).

### `rate_limited`

**HTTP status:** 429 · **Type:** `rate_limited` · **Retryable:** yes

Too many requests. Wait for `retry_after_ms` and retry with backoff.

### `sign_in_link_rate_limited`

**HTTP status:** 429 · **Type:** `rate_limited` · **Retryable:** yes

Too many sign-in codes were asked for this address or from your network. Use the newest code in your inbox, or wait 15 minutes and ask again.

At most 5 codes per email address in 15 minutes and 60 per network address in an hour, for every address alike, so the limit doesn't tell whether an account exists.

### `signup_rate_limited`

**HTTP status:** 429 · **Type:** `rate_limited` · **Retryable:** yes

Too many new accounts were created from your network recently. Wait an hour and try again, or sign in to an existing account.

## Tax blocked

### `evidence_conflict`

**HTTP status:** 422 · **Type:** `tax_blocked` · **Retryable:** no

We need to confirm the customer's country before we can calculate tax. Collect another piece of location evidence (billing address, card country, or the customer's confirmation of their country) and quote again.

The customer's location evidence (spec §13.2) disagrees, or there isn't enough of it for the regime's rule (`requiredNonConflicting` agreeing items, e.g. 2 for EU consumers). Under the `majority` policy a 1–1 tie has no majority yet; a third item (usually the card country) decides it. Under `hold_for_review` any conflict blocks until the customer confirms their country (self-declaration plus one matching item) or an operator resolves it. `details` names the countries seen and the rule applied.

### `jurisdiction_blocked`

**HTTP status:** 422 · **Type:** `tax_blocked` · **Retryable:** no

Sales to this jurisdiction aren't possible yet. This jurisdiction isn't covered by usable tax knowledge for this seller. Contact support if you need to sell there.

The engine needs a regime it doesn't have: the seller's home-country regime, an EU OSS scheme, a US state regime or a foreign-seller scheme is missing from the knowledge base or not reviewed for this mode, more than one regime would apply at once (stacked taxes are not supported), or the entity's registration marks the regime as not applicable. `details.regime` names it when known.

### `rate_unavailable`

**HTTP status:** 422 · **Type:** `tax_blocked` · **Retryable:** no

There's no usable tax rate for this product where the customer is. Sell to this jurisdiction later, once its rate is in the knowledge base and reviewed. In test mode, check that the jurisdiction is covered.

No rate is valid at the tax point for the jurisdiction and tax category (nor a `*` standard rate), the knowledge has more than one candidate, or the rate record has unsupported rules (e.g. a partial taxable base the engine can't express). Live mode uses only reviewed or verified records (spec §7.2, §9.1 step 7).

### `registration_required`

**HTTP status:** 422 · **Type:** `tax_blocked` · **Retryable:** no

The seller isn't registered for tax where this customer is, so new sales there are paused. Register the entity for the regime named in `details.regime` (see its prerequisites and deadline), or arm the jurisdiction once the prerequisites are done. The customer can join the waitlist meanwhile.

Returned by the registration gate (spec §10.4): the regime is `blocked_b2c` (a zero-threshold regime that isn't armed, a missed grace deadline, or a US state before registration with `usBehaviorBeforeRegistration: block`). Existing customers' renewals and top-ups continue (spec §10.7). `details` carries the regime, the registration state and, where known, the deadline and prerequisites.

### `tax_id_unavailable`

**HTTP status:** 422 · **Type:** `tax_blocked` · **Retryable:** yes

The customer's tax ID couldn't be checked right now. Try again in a few minutes. The tax-ID register (e.g. VIES) is unavailable, and this brand blocks business sales until the ID is verified.

The brand's `taxIdUnavailable` policy is `block`. With the default `charge_as_b2c`, the sale is taxed as a consumer sale instead and can be corrected by a credit note once the ID is validated (spec §9.1 step 1).

### `unsupported_tax_category`

**HTTP status:** 422 · **Type:** `tax_blocked` · **Retryable:** no

This product's tax category can't be sold to this customer yet. Sell it only where it is supported (e.g. domestically), or change the product's tax category if it was set wrongly.

The regime has no place-of-supply rule for the line's tax category and customer type, or the rule is `not_supported` (e.g. live online courses to consumers in another country). Ownrecord never guesses a place of supply (spec §9.1 step 5). `details.lines` lists the affected lines and categories.

## Insufficient funds

### `credit_balance_insufficient`

**HTTP status:** 402 · **Type:** `insufficient_funds` · **Retryable:** no

The customer's credit balance is smaller than the amount taken back. Take back at most the current balance (`details.balance`, integer micros, in `details.currency`). The credit balance never goes below zero.

A negative adjustment removes credit the customer hasn't used yet. Credit already applied to invoices can't be taken back this way: void the invoice or issue a credit note instead.

### `insufficient_funds`

**HTTP status:** 402 · **Type:** `insufficient_funds` · **Retryable:** no

There aren't enough funds for this amount. Reserve or capture a smaller amount, or top up the wallet first. `details.requested` and `details.available` are in integer micros.

Returned when a wallet's spendable credits (unexpired and not already reserved) are less than the amount asked for, or when a posting would take a wallet or credit balance below zero (spec §11.1, §17.2).

### `wallet_fresh_balance_capped`

**HTTP status:** 402 · **Type:** `insufficient_funds` · **Retryable:** no

Credits bought recently can't be spent beyond the brand's cap yet. Ask for less now, or wait until `details.freeAt`: the brand caps how much of a fresh balance can be used (`details.cap`, micros) until the credits are `details.untilAgeHours` hours old.

The brand's usage cap for fresh balances (spec §20 `wallet.usageCapUntilAgeHours`): credits a payment brought less than `untilAgeHours` ago can be reserved or spent up to `cap` in total. The wallet has the credits, but `details.locked` micros of them are held back until they're older. Older credits are never capped.

## Approval required

### `approval_required`

**HTTP status:** 202 · **Type:** `approval_required` · **Retryable:** no

This request needs another person's approval before it runs. 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.

Not an error: REST answers **HTTP 202** with `{ approval: { id, status, approveUrl, expiresAt, summary,
diff, … } }` and MCP with a result `{ approval_required: true, approval, … }` (not `isError`). The
organization's approval policy requires a person's approval for this operation (or for this amount): a
two-person rule, or, for agents, apps and MCP clients, the operation's risk tier (compliance and security
always) or an amount limit. When someone approves it, the operation runs exactly as requested: same input,
same Idempotency-Key, as you. Approvals expire after 72 hours.

## Processor error

### `assistant_unavailable`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** yes

The assistant is unavailable right now. Try again in a few minutes. Everything else in the console keeps working, and nothing was changed.

### `dispute_outcome_unknown`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** no

The processor closed a dispute with a status Ownrecord doesn't know. Nothing was recorded. Check the dispute in the processor's dashboard; platform support updates Ownrecord's status mapping. `details.status` is the processor's status.

Raised by the processor-event job, never by an API call: a `dispute.closed` event whose status is none of `won`, `lost`, `warning_closed` or `prevented`. The job stops (platform staff are alerted) rather than guess the outcome.

### `domain_dns_unavailable`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** yes

DNS didn't answer for the domain's ownership record. Retry after a short wait. Nothing was changed.

Ownrecord looks up the `_ownrecord-challenge` TXT record of the domain; the lookup failed (a timeout or a DNS server error, `details.code`). An answer that the record doesn't exist isn't an error: the domain stays `pending`.

### `domain_provider_error`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** yes

The domain provider couldn't complete the request. Retry after a short wait. If it keeps failing, contact support with the request ID.

`details.status` and `details.code` carry the provider's answer when there was one. Nothing was changed.

### `einvoice_provider_failed`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** yes

The e-invoicing provider couldn't take the document. Ownrecord retries on its own while the provider may recover. If the e-invoice ends up `failed`, read its error and retry it with `einvoices.retry` once the cause is fixed.

`details.provider` names the provider and `details.code` its own error code when it gave one.

### `fx_feed_invalid`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** yes

The ECB reference-rate file couldn't be read. Nothing was stored. The rates job retries automatically; if it keeps failing, the ECB file format may have changed.

Raised by the `fx_rates_fetch` job when the ECB XML is malformed: not the ECB envelope, unbalanced elements, an invalid date, currency or rate, a duplicate day or currency, or no rates at all. The whole file is rejected; no partial data is stored.

### `fx_feed_unavailable`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** yes

The ECB reference-rate file couldn't be downloaded. The rates job retries automatically with backoff. Missing days are backfilled from the 90-day file.

### `processor_payment_declined`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** no

The payment was declined. Ask the customer for another payment method; `details.decline_code` says why the card was declined.

The card issuer or the processor declined the payment. `details` has `processor`, `code` and `decline_code` (e.g. `insufficient_funds`).

### `processor_request_failed`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** yes

The payment processor couldn't complete the request. If `retryable` is true, retry with the same Idempotency-Key after a short wait; otherwise check `details.code`.

Returned when a call to the processor fails: an outage, a rate limit, a timeout (retryable), or a request the
processor refused (not retryable). `details` has `processor`, the processor's own `code` and `http_status`.
Nothing was changed in Ownrecord. When requests to a connection keep failing because the processor doesn't
answer, Ownrecord marks the connection `degraded` and new checkouts use the brand's secondary (spec §15.4).

### `upstream_timeout`

**HTTP status:** 502 · **Type:** `processor_error` · **Retryable:** yes

An outside service didn't answer in time. Retry the request with the same Idempotency-Key after a short wait.

Returned when an interactive call to an outside service (e.g. a tax ID check or a payment processor) times out before the operation could continue. Nothing was changed.

## Internal

### `checkout_fulfilment_invalid`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** no

The paid checkout's records don't add up, so it wasn't fulfilled. Contact support with the request ID. Nothing was recorded; the payment is reviewed by hand (a reconciliation alert is open).

### `credit_note_pdf_unavailable`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** yes

The stored credit note PDF couldn't be read. Try again in a few minutes. If it keeps failing, contact support with the request ID: the file is checked against its stored SHA-256 and never served when it differs.

### `domain_provider_not_configured`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** no

Custom domains aren't set up on this deployment. Contact support: the deployment's domain provider credentials are missing.

The deployment has neither development access (the fake provider) nor the Vercel API settings.

### `fx_rate_invalid`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** no

An exchange rate isn't a valid positive decimal number. This is a data problem on our side. Contact support with the request ID.

Rates are stored as decimal strings (e.g. `1.0912`) and converted with exact rational arithmetic. A stored rate that isn't a positive decimal is never used.

### `internal_error`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** yes

Something went wrong on our side. Retry later. If it persists, contact support with the request ID.

### `invoice_pdf_unavailable`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** yes

The stored invoice PDF couldn't be read. Try again in a few minutes. If it keeps failing, contact support with the request ID: the file is checked against its stored SHA-256 and never served when it differs.

### `jurisdiction_timezone_invalid`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** no

The jurisdiction has no usable time zone, so no deadline can be calculated. This is a problem on our side. Contact support with the request ID.

Registration deadlines fall at the end of a day in the jurisdiction's time zone (spec §10.3), recorded as an IANA zone such as `Europe/Zagreb` in the knowledge base. `details` names the zone or the regime.

### `registration_grace_rule_invalid`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** no

The regime's grace rule is incomplete, so no deadline can be calculated. This is a problem on our side. Contact support with the request ID.

A `days_after_trigger` rule needs `days` and an `nth_day_of_following_month` rule needs `nthDay` (1–31) in the regime record of the knowledge base (spec §10.3). `details.rule` and `details.problem` describe it.

### `service_maintenance`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** yes

Ownrecord is down for maintenance. Try again after the time in the `Retry-After` header. The status page says when the maintenance ends.

Answered with HTTP 503 and a `Retry-After` header to every request while platform staff run maintenance (a database restore). Nothing was changed: retry the same request, with the same `Idempotency-Key`, later.

### `tax_fx_rate_required`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** no

The sale must be converted to a threshold's currency, but no exchange rates were supplied. This is a problem on our side. Contact support with the request ID.

The registration gate compares this sale with a threshold in another currency (spec §10.5) and needs the ECB rates of the tax point. `details` names both currencies.

### `tax_snapshot_mismatch`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** no

The tax knowledge used for this quote doesn't match the quote's tax point or mode. This is a problem on our side. Contact support with the request ID.

The knowledge snapshot must be built for the tax point's calendar date (within one day of its UTC date) and, for a live-mode entity, with live-mode gating (spec §7.2, §9).

### `threshold_rule_incomplete`

**HTTP status:** 500 · **Type:** `internal` · **Retryable:** no

The regime's threshold is incomplete, so its counter can't be kept. This is a problem on our side. Contact support with the request ID.

A threshold counter needs a currency: the threshold amount's, or for a count-only threshold the currency of the regime's jurisdiction (spec §10.5). `details.regime_id` names the regime.
