---
title: Amazon Connect + Odoo 19 (Oct 2026): JSON-2 Setup, API Keys, Phone Match
description: On October 28, 2026 an Odoo 19 lookup from Amazon Connect needs a Custom plan, a bot user with an API key that expires within 3 months, and a phone match that survives formatting. Step-by-step setup, test plan and troubleshooting.
url: https://www.factualminds.com/blog/amazon-connect-odoo-integration-2026/
datePublished: 2026-10-28T00:00:00.000Z
dateModified: 2026-10-28T00:00:00.000Z
author: palaniappan-p
category: Cloud Architecture
tags: amazon-connect, contact-center, aws, odoo, erp, aws-lambda
---

# Amazon Connect + Odoo 19 (Oct 2026): JSON-2 Setup, API Keys, Phone Match

> On October 28, 2026 an Odoo 19 lookup from Amazon Connect needs a Custom plan, a bot user with an API key that expires within 3 months, and a phone match that survives formatting. Step-by-step setup, test plan and troubleshooting.

On October 28, 2026, an Amazon Connect flow can ask Odoo 19 "who is calling and what do they owe?" with three small JSON-2 requests, provided four things are true: your plan includes the external API, a dedicated bot user holds an API key that is at most three months old, the Lambda reaches the Odoo URL inside the flow's 8 seconds, and the caller's phone number matches however it was typed into Odoo. This post is the Odoo-only walkthrough. For the comparison across NetSuite, Odoo and JD Edwards, start with the [parent ERP post](https://www.factualminds.com/blog/amazon-connect-erp-integration-netsuite-odoo-jd-edwards-2026/). For the Bedrock-agent angle on the same ERP, see [Amazon Bedrock and Odoo](https://www.factualminds.com/blog/amazon-bedrock-odoo-integration/); this one is a plain lookup with no model in the loop.

The shared Connect rules, in three sentences: a synchronous **Invoke AWS Lambda function** block waits at most 8 seconds, the response must be a flat map of strings in `[A-Za-z0-9_-]` under 64 KB, and the handler should never throw, returning a `FALLBACK` status the flow can branch on. Everything below is Odoo-specific. Broader Connect work is on [Amazon Connect consulting](https://www.factualminds.com/services/amazon-connect/).

We read Odoo's 19.0 documentation and the 19.0 source files on GitHub on October 28, 2026. The unit tests run against mocked HTTP. We also ran the unmodified adapter against a local Odoo 19.0 container on October 28, 2026; the numbers are in the benchmark section. Where we did not run something, we say so.

---

## Step 1: Check the plan and the version first

Two gates can end the project before any code.

Plan. The External JSON-2 API page states that access to data via the external API is only available on Custom Odoo pricing plans, and is not available on One App Free or Standard. The page says nothing about how this applies to Odoo.sh or self-hosted installations. We could not verify it, so do not guess: ask your Odoo Customer Success Manager for a written answer that names the API and your hosting model.

Version. The page is marked "versionadded 19.0". The 17.0 and 18.0 documentation pages for the same path carry the Custom-plan note, but not the JSON-2 section. If you run Odoo 17 or 18 on Odoo.sh or on-premise, the JSON-2 adapter in our artifact is not for you. Odoo Online tracks current versions, so it is the simple case.

### The deprecation dates, settled

The parent post said the dates had moved. Here is the sequence from Odoo's own repository:

| Source | What it says |
| --- | --- |
| Odoo 19.0 docs, October 28, 2026 | `/xmlrpc`, `/xmlrpc/2`, `/jsonrpc` scheduled for removal in **Odoo 22 (fall 2028)** and **Odoo Online 21.1 (winter 2027)** |
| Documentation commit, February 4, 2026 | Removal "postponed to Odoo 21.1/22"; it had first been planned for 19.1/20 |
| Documentation commit, October 9, 2026 | Added the Online 21.1 wording to the page text |

So the earlier date you may have read is superseded. The practical meaning: a new build should use JSON-2 now, because on Odoo Online the old endpoints go first (winter 2027). Other controllers declared with `@route(type='jsonrpc')` (known as `type='json'` until Odoo 18) are explicitly not covered by the notice.

## Step 2: Create a dedicated bot user

Odoo's page says the JSON-2 API validates every operation against the access rights, record rules and field access of the user whose key is used. For integrations it recommends dedicated bot users: minimum permissions, so a leaked key does little damage; an empty password, which disables login-and-password sign-in; and audit fields (create and write user) that point at the bot, so nobody is impersonated.

Create one internal user, for example `connect-lookup`, and grant only what the three reads need:

| Read | Model | Group in the 19.0 source |
| --- | --- | --- |
| Find the customer | `res.partner` | Any internal user (`base.group_user`) can read partners subject to record rules |
| Count open invoices | `account.move` | **Show Accounting Features - Readonly** (`account.group_account_readonly`) |
| Latest order | `sale.order` | **User: All Documents** (`sales_team.group_sale_salesman_all_leads`), or **Own Documents Only** if the bot owns nothing and you accept empty results |

Two cautions. First, the Sales groups are not read-only; **User: All Documents** carries write access to sales records. If a leaked key must not be able to edit orders, create a custom group whose access rules (`ir.model.access`) grant read only on these models, and test with the bot's key. We did not build or test that group. Second, multi-company databases add record rules by company, which can hide invoices from a bot in the wrong company. Test with a real partner in each company.

Whether an extra internal user changes your Odoo subscription cost is a licensing question we cannot answer from the documentation.

## Step 3: Create the API key and plan its rotation

In Odoo, open the bot user's **Preferences**, then **Account Security**, then **New API Key**. Per the documentation:

- A description and a duration are both required.
- Odoo says it is not possible to create keys that last for more than three months, so long-lasting keys must be rotated at least every three months. Short durations (typically one day) are advised for interactive use.
- The **Generate Key** button creates a 160-bit random key, shown once.
- Keys are bound to the user and inherit that user's rights. There is no separate scope list.

Programmatic management exists too: a user can generate up to 10 keys through the API by default, adjustable with the system parameter `base.programmatic_api_keys_limit`, and exceeding it returns HTTP 422. For a Connect integration we would not automate this on day one. A calendar reminder at week 10 and a runbook are cheaper than a rotation Lambda that itself needs a key.

A rotation that causes no outage works like this:

1. Create the second key (new description, new expiry) while the first still works.
2. Write the new value into the Secrets Manager secret.
3. Wait for warm Lambda containers to refresh their cached copy (set a cache time-to-live, not "cache forever").
4. Revoke the first key in Odoo.

The sample reads `ODOO_API_KEY` from the environment for simplicity. In production, read it from AWS Secrets Manager at init and cache it with an expiry, otherwise step 3 never completes until a redeploy.

## Step 4: Know the request, the headers and the database

JSON-2 is `POST /json/2/<model>/<method>` with a JSON body. Headers per the 19.0 page:

| Header | Status |
| --- | --- |
| `Host` | Required (set by every HTTP client from the URL) |
| `Authorization: bearer <API key>` | Required |
| `Content-Type: application/json` | Required |
| `X-Odoo-Database` | Optional; **required** when one server hosts several databases and `dbfilter` does not select by `Host` |
| `User-Agent` | Recommended |

Odoo Online usually maps one hostname to one database, so the header is rarely needed. On a self-hosted multi-database server it is the first thing to check. The adapter sends `X-Odoo-Database` only when `ODOO_DB` is set.

Body keys: `ids` for record methods, `context` for an object such as `{"lang": "en_US"}`, then the method's parameters by name. A success is HTTP 200 with the return value as JSON. An error is a 4xx or 5xx with an object containing `name`, `message`, `arguments`, `context` and `debug`. The `debug` field is a traceback. Never log the body to a place customers can read.

Each call runs in its own SQL transaction, and you cannot chain calls. For a read-only lookup this is harmless. It is also why the adapter uses `search_read`, which Odoo's page names as the example of one method doing `search` and `read` consistently in a single transaction.

## Step 5: The three reads, and the fields behind them

This is what [`odoo.mjs`](https://www.factualminds.com/examples/architecture-blog-2026/connect-commerce-context/erp/odoo.mjs) does, in order:

1. `res.partner/search_read` with `domain: [["phone","=",<caller>]]`, `fields: ["id","active"]`, `limit: 2` and `context: {"active_test": false}`.
2. In parallel: `account.move/search_count` and `sale.order/search_read` for that partner.

So the path is three requests with two in flight at once.

### Partner: `phone`, not `mobile`

In the Odoo 19.0 source, `res.partner` in the base module defines `phone` and no `mobile`. The 17.0 and 18.0 sources define both. If you copy a tutorial that searches `mobile`, expect an error on 19 (the exact message is a test-database check for you). The 19.0 page also tells you that the models and fields in *your* database are listed on its own `/doc` page, which is the authority when custom modules are involved.

### Archived partners: `active_test: false`

Odoo's ORM hides records whose `active` is false from searches. A customer you archived last year is therefore invisible unless the context turns the filter off. The adapter passes `context: {"active_test": false}` so an archived partner comes back with `active: false` and the flow can say `INACTIVE`. One cost: archived duplicates also match, which feeds the next case.

### Shared numbers: `limit: 2`

Two partners on one number (a company and its contact, or a family) cannot be disambiguated by phone alone. The adapter asks for two rows; if it gets two, it returns `AMBIGUOUS` and reads nothing out. The 13 tests in the artifact include this case.

### Invoices: `account.move`

The count filters `move_type = out_invoice`, `state = posted` and `payment_state in (not_paid, partial)`. Per the 19.0 source, `payment_state` can also be `in_payment`, `paid`, `reversed`, `blocked` and `invoicing_legacy`. Decide deliberately:

- `in_payment` (payment registered, not yet reconciled) is not counted as open by the adapter. Whether the IVR should call that "paid" is a finance decision.
- `blocked` and credit notes (`out_refund`) are excluded. If finance wants disputed invoices counted, add `blocked`.

### Orders: `sale.order`

`state` in 19.0 is one of `draft` (Quotation), `sent` (Quotation Sent), `sale` (Sales Order) or `cancel`. There is no separate "done" state in the 19.0 source; locking is a distinct boolean. The adapter returns the raw state and the order `name`, so the flow sees `sale` and `S00042`. Map `sale` to "confirmed" in your prompt copy, not in the adapter.

## Step 6: Phone normalisation, the part that decides your hit rate

Connect hands the Lambda the caller ID in E.164 (`+14155550123`), and the handler rejects anything else before it reaches a query. The shipped adapter then does an exact match on `res.partner.phone`. Odoo stores whatever a person typed, so `+1 415 555 0123` and `(415) 555-0123` both miss, and the flow says `NOT_FOUND` for a real customer. This is the largest source of silent wrong answers, and the adapter, as written, does not solve it.

What the 19.0 source offers, read but **not run by us**:

- The `phone_validation` module (hidden category, `auto_install` when `base` and `mail` are present, so most databases have it) adds a **stored** field `phone_sanitized` to `res.partner`, described in the source as holding the sanitized number "to speed up searches and comparisons". It is computed from the phone field using the partner's country and company context. Treat a blank value as "could not parse".
- The same module adds a non-stored, search-only field `phone_mobile_search`. Its search strips punctuation from stored values and, for an input starting with `+` or `00`, matches both the `+` and `00` forms. It raises an error for search terms shorter than 3 characters.

The lowest-risk change we would make is to search `["phone_sanitized","=","+14155550123"]` first, falling back to `phone_mobile_search` only if you see misses in testing. Two caveats the source shows: the module code skips `phone_sanitized` in the combined search on databases where its index is missing, and `phone_sanitized` depends on the partner's country, so partners with no country and a national-format number may stay blank. Neither field name is guaranteed to exist if `phone_validation` was uninstalled. Check both on your `/doc` page.

The fix is data quality: one pass that rewrites partner phones to international format, and a validation rule on entry.

## First-party benchmark: the adapter against a real Odoo 19 container

On October 28, 2026 we ran the unmodified `erp/handler.mjs` and `erp/odoo.mjs` against Odoo 19.0 (Docker image `odoo:19.0`) with PostgreSQL 16, Sales and Invoicing installed and no demo data. The dataset was 202 partners (two of them sharing one phone), 67 posted customer invoices and 200 sales orders. Node v26.12.1 called the JSON-2 API with a real API key over localhost on an Apple M4.

| Metric | Result |
| --- | --- |
| Calls (after 5 warm-up calls) | 200, sequential |
| p50 | 10 ms |
| p95 | 11 ms |
| max | 13 ms |
| Statuses | 172 `OK`, 28 `AMBIGUOUS` |

What the run confirmed beyond latency: the JSON-2 calls the adapter makes work on 19.0 as written, including `context: {active_test: false}` on `res.partner`, `payment_state` on `account.move` and `state` on `sale.order`; the shared phone returned `AMBIGUOUS`, not the first partner; an unknown number returned `NOT_FOUND`.

What it does not tell you: network distance to your Odoo host, TLS, Odoo under concurrent load or with a large database, Connect-to-Lambda overhead and Lambda cold starts. On a real deployment the network round trips dominate: the adapter makes one call and then two in parallel, so budget roughly two round trips plus Odoo's query time against the 5-second abort. Measure from a Lambda in your Region against staging. Setup, seed script and raw results: [benchmark folder](https://www.factualminds.com/examples/architecture-blog-2026/connect-commerce-context/benchmark/README.md).

## Step 7: Networking from Lambda to Odoo

| Hosting | Path | What to check |
| --- | --- | --- |
| Odoo Online | Public HTTPS to your `*.odoo.com` host | Nothing in a VPC is required. Confirm the plan |
| Odoo.sh | Public HTTPS to the project URL | We did not verify IP allow-listing options. Ask |
| On-premise, public | HTTPS through your reverse proxy | `Host` and `X-Odoo-Database` routing, TLS certificate chain |
| On-premise, private | Lambda in a VPC over Site-to-Site VPN or Direct Connect | A VPC endpoint or NAT route to reach Secrets Manager; every hop spends the same 8 seconds |

A Lambda inside a VPC with no internet route cannot reach Secrets Manager or a public Odoo. Measure round-trip time from the Lambda's own subnet. If p95 for the three-request path is above about 3 seconds there, use the cached-summary pattern from the parent post instead of a live call.

## Step 8: A test plan you can run in order

Our benchmark container covered part of step 4 (an active partner with a posted invoice, two partners on one number, an unknown number) and step 5, using the admin user's API key rather than a restricted bot user. Steps 1 to 3 and 6 to 8 we have not run. They are the sequence we would follow, cheapest first.

1. Entitlement. From a laptop, `POST` any harmless call (for example `res.partner/search_count` with `{"domain": []}`) with the bot key. HTTP 200 means plan and key work. A 401 or 403 means stop and fix before any Lambda.
2. Plan and version evidence. Open `https://<host>/doc` and confirm `res.partner`, `account.move` and `sale.order` list the fields from Step 5 and whether `phone_sanitized` exists.
3. Rights. Repeat the three reads with the bot key. Then try one write (for example, update a partner's `comment`) in a test database; it should fail with an access error. If it succeeds, tighten the group.
4. Data cases in a test database. One active partner with an unpaid posted invoice; one archived partner; two partners on one number; one number stored as `(415) 555-0123`; one partner with no orders. Expected adapter output: `ACTIVE` with count; `INACTIVE`; `FALLBACK/AMBIGUOUS`; `NOT_FOUND` (documents the gap); `lastOrderStatus=NONE`.
5. Mock suite. `cd examples/architecture-blog-2026/connect-commerce-context/erp && node --test`. The suite is mocked and checks output shape, timeout, HTTP 503, 401, ambiguity, archived partners and that logs never contain the phone.
6. Lambda in situ. Invoke with `event.sample.json` (`erp: "odoo"`) using the real secrets and read the single JSON log line: `lookupStatus`, `fallbackReason`, `ms`.
7. Flow. Use a test number, branch on `lookupStatus`, and place a call and a chat.
8. Expiry drill. Create a key with a one-day duration, let it expire, and confirm the flow lands on `HTTP_401` and the metric-filter alarm fires.

## Troubleshooting

| Symptom | Likely cause | Check |
| --- | --- | --- |
| `fallbackReason=HTTP_401` | Key expired, revoked, or wrong database | Key duration is at most 3 months; send `X-Odoo-Database` if the server hosts several databases |
| `HTTP_403` | Bot lacks access to a model or a record rule hides it | Re-run Step 8 item 3 as the bot |
| `HTTP_404` on every call | The server has no `/json/2` route (Odoo before 19), or the URL is wrong | Version; `ODOO_URL` has no trailing path |
| `HTTP_400` or `HTTP_500` on partner search | Field missing in your database (`mobile`, custom module) | The error body's `message`; the `/doc` page |
| Plan error or access refused for a valid key | Plan does not include the external API | Written confirmation from Odoo |
| `NOT_FOUND` for a known customer | Phone stored in a different format | Search the partner by name in Odoo and compare the stored phone |
| `INACTIVE` never appears | Context missing, or a proxy strips the body | Test the archived partner in Step 8 item 4 |
| `AMBIGUOUS` more than expected | Company and contact share a number, or archived duplicates | Dedupe, or key on `phone_sanitized` plus partner type |
| `TIMEOUT` | Cold start plus network plus three requests exceed the 5 s budget | Measure p95 from the Lambda subnet; consider the cached pattern |
| `NOT_CONFIGURED` | `ODOO_URL` or `ODOO_API_KEY` missing | Environment or secret retrieval |
| `BAD_RESPONSE` | HTTP 200 without `accountStatus` | Proxy or error page returning 200 |

Note the exact `HTTP_<status>` strings come from the handler; Odoo's own message sits in the response body, which the sample deliberately does not forward to the flow.

## Security notes

- The key acts as its user. A bot with an empty password and minimum groups limits the blast radius; a personal account does not.
- Keep the key in Secrets Manager with a rotation calendar. Do not put it in Lambda environment variables in production.
- Return four fields to the flow. Values saved with **Set contact attributes** appear in contact records.
- Odoo's error body includes a Python traceback in `debug`. Keep it out of contact attributes and customer-facing logs.
- Caller ID can be spoofed. Do not read out balances or order details on a phone match alone; require verification for anything beyond status.
- Customer and invoice data are personal data. Running on AWS does not make the handling compliant.

## Costs (published only)

- Lambda: the published free tier is 1 million requests and 400,000 GB-seconds a month; see the [Lambda pricing page](https://aws.amazon.com/lambda/pricing/).
- Secrets Manager: per secret per month and per 10,000 API calls; init-time caching keeps calls near the cold-start count.
- Site-to-Site VPN or Direct Connect: only for private on-premise Odoo; see the [VPN pricing page](https://aws.amazon.com/vpn/pricing/).
- Amazon Connect: on October 28, 2026 Connect Customer lists chat at $0.010 per message and voice at $0.038 per minute, telephony extra, per the [Connect pricing page](https://aws.amazon.com/connect/pricing/). The lookup adds Lambda cost, not a separate Connect charge.
- Not published: the Odoo Custom plan, any extra user cost for a bot user, and Odoo.sh or hosting. We list no figure for them.

## Reproduce this

> **Reproduce this** — The Odoo adapter is [`odoo.mjs`](https://www.factualminds.com/examples/architecture-blog-2026/connect-commerce-context/erp/odoo.mjs), called by [`handler.mjs`](https://www.factualminds.com/examples/architecture-blog-2026/connect-commerce-context/erp/handler.mjs), with the [README](https://www.factualminds.com/examples/architecture-blog-2026/connect-commerce-context/erp/README.md) and a sample event. Run `node --test` in that folder; all HTTP is mocked. It has not been run against a live Odoo in this post's text, apart from any benchmark section above.

## What to do this week

1. Get written confirmation that your Odoo plan and hosting include the external API.
2. Open `/doc` on a test database and confirm the three models and the phone fields.
3. Create the bot user, its key with an expiry, and a calendar entry for rotation.
4. Run Step 8 items 1 to 4 before touching a flow.
5. Decide how to count `in_payment` and `blocked` invoices with finance.
6. Choose the phone strategy: clean the data, or switch the search to `phone_sanitized`.

## What this post doesn't cover

- A live run against a production or Odoo.sh database. The benchmark used a local container with 202 partners; the unit tests are mocked.
- Whether Odoo.sh or self-hosted installations are entitled to the external API, and any IP allow-listing on Odoo.sh.
- Odoo 17 and 18 (XML-RPC and JSON-RPC adapters).
- Writes back to Odoo, and a read-only custom group, which we did not build.
- The cached-summary pattern; see the [parent post](https://www.factualminds.com/blog/amazon-connect-erp-integration-netsuite-odoo-jd-edwards-2026/).
- Compliance obligations and Odoo licence cost.

For how we approach the commerce side of Connect, see [Amazon Connect for eCommerce](https://www.factualminds.com/services/amazon-connect/aws-retail-ecommerce/).

## FAQ

### Which Odoo versions can an Amazon Connect Lambda call with the JSON-2 API?
Odoo 19.0 and later. The Odoo 19.0 documentation marks the External JSON-2 API as added in 19.0, and the 17.0 and 18.0 documentation pages do not describe it. On Odoo 17 or 18 the external API is XML-RPC or JSON-RPC, which is a different adapter. Odoo Online runs the current version, so a JSON-2 adapter fits there.

### When are the XML-RPC and JSON-RPC endpoints removed?
The Odoo 19.0 documentation says /xmlrpc, /xmlrpc/2 and /jsonrpc are scheduled for removal in Odoo 22 (fall 2028) and Odoo Online 21.1 (winter 2027). A documentation commit dated February 4, 2026 says the removal was first planned for 19.1 and 20 and was postponed to 21.1 and 22. An earlier removal date you may have seen is the superseded one.

### How long can an Odoo API key live?
At most three months. Odoo's documentation says keys cannot be created that last longer, so long-lasting keys must be rotated at least once every three months. A key is shown once at creation. Put rotation on a calendar and in a runbook before go-live.

### Does the Odoo external API work on every plan?
No. The documentation says access to data via the external API is only available on Custom pricing plans and not on One App Free or Standard. It does not say how this applies to Odoo.sh or self-hosted installations, so confirm entitlement with your Odoo contact in writing.

### Why does the lookup use the phone field and not mobile?
In the Odoo 19.0 source the base res.partner model defines phone but no mobile field. The 17.0 and 18.0 sources define both. If your database was upgraded, or has a custom module, check the field list on your own /doc page before relying on either.

### Why does the adapter send active_test false?
Odoo hides archived records from searches by default. Without a context of active_test false, an archived partner is not found, so the Connect flow would report NOT_FOUND for a customer that exists and could never report INACTIVE.

### Can the bot user be read-only?
Partly. Odoo recommends a dedicated bot user with the minimum permissions and a password left empty. In the 19.0 source the Accounting group named Show Accounting Features - Readonly exists, but the Sales groups are not read-only: User: All Documents can edit. A strictly read-only integration needs a custom group with read-only access rules, which you must test in your own database.

---

*Source: https://www.factualminds.com/blog/amazon-connect-odoo-integration-2026/*
