Skip to content

Bank reconciliation integration

Accountify owns the review workflow; Odoo owns the accounting mutation. The recommended design uses a Studio automated-action webhook and server action to validate and execute one native Odoo reconciliation transaction.

flowchart LR UI["Accountify UI: queue, suggestions, approval"] Backend["Accountify backend: auth, locks, idempotency, audit"] Statement["Odoo statement line"] Candidates["Odoo open move-line residuals"] Wrapper["Studio webhook wrapper"] Ledger["Native Odoo reconciliation"] UI --> Backend Backend --> Statement Backend --> Candidates Backend -->|Signed request| Wrapper Wrapper --> Ledger Ledger -->|Readback| Backend

Supported boundary

Component Models Operation
Queue account.bank.statement.line Read unreconciled bank/card transactions
Candidate engine account.move.line Read posted open residuals on reconcilable accounts
Configuration account.journal, account.account Read suspense account and reconciliation capability
Mutation wrapper base.automation, ir.actions.server Validate request and invoke native reconciliation
Verification account.partial.reconcile, account.full.reconcile Read native result and residual state

Safe sequence

sequenceDiagram autonumber participant Staff as Staff user participant UI as Accountify UI participant API as Accountify backend participant Odoo as Odoo Studio wrapper participant Ledger as Odoo native ledger Staff->>UI: Approve selected candidates UI->>API: Submit guarded reconcile request API->>API: Authorize, lock, and reserve idempotency key API->>Odoo: Send signed payload and expected write dates Odoo->>Odoo: Validate company, state, residuals, and lock dates Odoo->>Ledger: Call native reconcile in one transaction Ledger-->>Odoo: Reconciliation result Odoo-->>API: Return status API->>Ledger: Read back statement and move-line state API-->>UI: Verified success or actionable exception
  1. Read unreconciled statement lines with explicit company, amount, currency, reference, partner, state, and write_date fields.
  2. Read candidate move lines that are posted, unreconciled, on a reconcilable account, same-company, and have non-zero residuals.
  3. Rank suggestions in Accountify and explain amount, partner, reference, date, currency, journal, ecommerce, and historical signals.
  4. Require user approval; acquire a tenant/statement-line lock and a durable idempotency key.
  5. Send a signed request to the tenant's versioned Studio webhook.
  6. Revalidate all state in Odoo immediately before mutation.
  7. Invoke the native reconciliation method in the wrapper's transaction.
  8. Read back statement, move-line residual, matching, and full/partial reconciliation state before marking success.

Webhook request contract

{
  "request_id": "rec_01J...",
  "idempotency_key": "tenant:statement:selection:version",
  "secret": "redacted",
  "statement_line_id": 123,
  "selected_move_line_ids": [456, 789],
  "action": "exact_match",
  "allow_partial": false,
  "expected_statement_write_date": "2026-06-29 09:10:11",
  "expected_selected_write_dates": {
    "456": "2026-06-29 08:00:00",
    "789": "2026-06-29 08:05:00"
  },
  "client_context": {
    "tenant_id": "tenant_123",
    "actor_id": "staff_456"
  }
}
Field Required Validation
request_id Yes Unique audit/correlation identifier
idempotency_key Yes Same semantic request must return/recover the same outcome
secret Yes Compare in Odoo; never expose it to the browser or logs
statement_line_id Yes Exists, same company, still unreconciled, expected write_date
selected_move_line_ids Yes Non-empty, posted, open, reconcilable, same company/account path
action Yes One of the approved modes below
allow_partial Yes Must be explicit; never infer partial approval
expected write dates Yes Reject stale statement/candidate state
client_context Yes Audit context only; never trust it for authorization

Actions

Action Meaning MVP
exact_match Selected residuals exactly offset the statement suspense amount Yes
partial_match Approved residual remains open Yes, explicit only
write_off Approved difference posted to a selected policy account Later
bank_fee Explicit bank-fee adjustment then reconciliation Later
transfer Same-entity bank/card transfer handling Later
mark_to_check No accounting mutation; route to manual review Yes

Every later action requires a passed sandbox-feasibility clone test before it is committed to a release: proof that its complete safe_eval call surface executes inside the wrapper. The sandbox permits public methods and mapped field writes only; underscore-prefixed methods are blocked. Feasibility evidence is valid per Odoo version and is re-validated by the scheduled drift check, because Odoo Online upgrades on Odoo's schedule.

Odoo-side validation

The wrapper must reject the request before mutation when any of these checks fail:

  • Secret, request shape, action, or idempotency validation.
  • Statement or candidate does not exist, changed since approval, or is already reconciled.
  • Records belong to different companies or violate allowed-company context.
  • Parent journal entries are not posted.
  • Candidate accounts are not reconcilable or do not form a valid native reconciliation set.
  • No unique statement suspense line can be identified.
  • Residual/currency amounts do not support the approved exact or partial action.
  • Accounting/tax lock date prevents mutation.
  • Adjustment action lacks an approved account, journal, explanation, or policy.
flowchart TD Start["Approved reconciliation request"] Invalid{"Current Odoo state still valid?"} Conflict["Reject as validation or concurrency conflict"] Exact{"Amounts exactly offset?"} Reconcile["Native exact reconciliation"] Partial{"Explicit partial approval?"} PartialReconcile["Native partial reconciliation"] Adjustment{"Approved adjustment policy and account?"} Adjust["Create explicit adjustment, then reconcile"] Review["Manual review"] Readback["Read back and classify final state"] Start --> Invalid Invalid -->|No| Conflict --> Readback Invalid -->|Yes| Exact Exact -->|Yes| Reconcile --> Readback Exact -->|No| Partial Partial -->|Yes| PartialReconcile --> Readback Partial -->|No| Adjustment Adjustment -->|Yes| Adjust --> Readback Adjustment -->|No| Review --> Readback

Response and readback

A webhook HTTP response is not proof of accounting success. The backend should classify the immediate result, then re-read:

  • statement line is_reconciled, move_id, and write_date;
  • selected move-line reconciled, residuals, matching_number, full/partial reconcile links, and write_date;
  • created adjustment move/lines when applicable;
  • native partial/full reconciliation records.

Use operational states: suggested, queued, locked, sent_to_odoo, verified_success, validation_failed, concurrency_conflict, repair_required, and manual_review.

Error taxonomy

Status Meaning Required action
validation_failed Contract or current Odoo state invalid Show exact validation; allow correction
concurrency_conflict write_date, residual, or reconcile state changed Reload and rescore; do not retry stale payload
webhook_error Wrapper rejected or crashed before confirmed result Record sanitized error; investigate/version check
odoo_accounting_error Native reconciliation/lock/tax/account rule rejected Manual accounting review
timeout_unknown Caller does not know whether Odoo committed Read back by idempotency and record state before retry
repair_required Odoo and Accountify states diverged Disable action for record/tenant and open repair task
manual_review Valid case outside automated policy Route with context and suggested next step

The backend must be able to assign these statuses without human inspection. The Studio webhook response is not guaranteed to carry a structured result, so while the webhook transport is in use the wrapper must expose rejection evidence the backend can read back — for example a rejection code and operation ID written to a Studio field — whenever the HTTP response alone cannot distinguish validation_failed from webhook_error.

Audit record

Record tenant and Odoo database, user/role, statement/journal/amount/date/reference/partner, candidate IDs and before residuals, approved action, idempotency key, wrapper version/hash, webhook identifier, timestamps/duration, sanitized error, readback state, and final status. Do not log API keys, webhook secrets, raw bank payloads, or unnecessary personal data.

Bootstrap and drift control

flowchart TB subgraph Provision["Provision tenant"] direction LR Connect["Connect tenant"] Discover["Discover models, fields, company, and ACL"] Deploy["Deploy versioned automation and action"] Store["Store webhook URL, UUID, version, and hash"] Connect --> Discover --> Deploy --> Store end subgraph Operate["Health and drift control"] direction LR Health["No-op health check"] Enable["Enable reconciliation UI"] Drift["Scheduled drift check"] Repair["Disable and repair"] Health --> Enable --> Drift Drift -->|Pass| Enable Drift -->|Fail| Repair end Provision --> Operate
  1. Discover required models and fields using the current runtime exporter.
  2. Create or update the automation and server action with a reviewed code version.
  3. Store webhook URL/UUID, wrapper version, and code hash in Accountify secrets/configuration.
  4. Execute a no-op health payload and verify the expected response.
  5. Enable the Reconcile control only after capability and permission checks pass.
  6. Recheck version, hash, URL, model fields, and ACLs on a schedule and after Odoo changes.

All bootstrap, read, and readback calls must use the JSON-2 API. The legacy /xmlrpc, /xmlrpc/2, and /jsonrpc endpoints are scheduled for removal in Odoo Online 21.1 (winter 2027) and Odoo 22 (fall 2028); client databases upgrade on Odoo's schedule, so no new tooling may depend on them. Bootstrap and operating credentials are separate: the operating identity used at runtime must not be able to write ir.actions.server code.

Open verification items

The June 26, 2026 smoke test proved wrapper deployability — automation and server-action create/update/read, webhook URL and UUID generation, and cleanup — over legacy JSON-RPC. It did not execute a reconciliation. The following must pass in a representative clone before production enablement:

  1. Transaction semantics. A payload that validates, mutates, then deliberately raises leaves no committed change; two concurrent deliveries for one statement line produce exactly one reconciliation.
  2. Sandbox surface. account.move.line.reconcile() and every required field write execute inside the Execute Code action without a sandbox violation.
  3. Least-privilege identity. The wrapper operates under a restricted integration user; the operating credential cannot write server-action code.
  4. Transport decision. Evaluate invoking the same versioned server action through JSON-2 ir.actions.server.run() with the payload in context (standard API-key authentication, Odoo access rights, structured error responses) against the webhook trigger; adopt one and record the decision.
  5. JSON-2 bootstrap. The full deployment smoke suite passes over /json/2 endpoints.

Create-type operations (adjustment moves for later actions) additionally require a proven duplicate-suppression pattern before enablement. Candidate techniques — anchor-row serialization and a uniqueness receipt on a core unique-constrained model — are clone-gated and not yet approved.

Test matrix

At minimum cover exact, partial, multi-item, over/underpayment, fee/write-off, transfer, refund/chargeback, duplicate import, stale write dates, double-click/retry, wrong company, draft entry, non-reconcilable account, lock date, multi-currency/exchange difference, missing suspense configuration, timeout-after-commit, and wrapper drift. Assert both final Odoo state and Accountify task state.

Design basis

This guide is the sanitized, version-controlled implementation contract derived from the restricted Odoo Online Reconciliation Wrapper - Studio Webhook Technical Design dated June 26, 2026. The Drive source folder remains the record for the full design narrative.