> For the complete documentation index, see [llms.txt](https://docs.cybus.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.cybus.io/2-4-2/guides/system-connectivity/mes-erp-business-systems/odoo-integration.md).

# Odoo Integration

This guide describes how to integrate Odoo with Connectware, using the Manufacturing (MRP) module as an example. You configure a service commissioning file that reads manufacturing orders from Odoo, logs shop floor events on manufacturing orders, and receives webhook notifications from Odoo automation rules. A complete example file is available at the end of this guide.

## Objectives

* Setting up an authenticated connection between Connectware and the Odoo external API.
* Reading manufacturing orders from the Odoo Manufacturing (MRP) module.
* Logging shop floor events as notes on Odoo manufacturing orders.
* Receiving webhook notifications from Odoo automation rules in Connectware.

## Prerequisites

To follow this guide, you will need the following:

* A running instance of Cybus Connectware.
* Access to an Odoo instance with the Manufacturing (MRP) app installed. The JSON-RPC calls in this guide work with Odoo 14.0 and later. For Odoo Online, access to the external API is only available on Custom pricing plans. For details, see the [Odoo external API documentation](https://www.odoo.com/documentation/18.0/developer/reference/external_api.html).
* An Odoo user with access to the Manufacturing app. You create an API key for this user as part of this guide.
* Access to the [Admin UI](/2-4-2/access/admin-ui.md) with sufficient [user permissions](/2-4-2/access/user-management.md).
* Basic knowledge of MQTT and the Connectware [services](/2-4-2/data-flows/services.md) concept (for example, [service commissioning files](/2-4-2/data-flows/service-commissioning-files.md), [connections](/2-4-2/data-flows/service-commissioning-files/resources/cybus-connection.md), and [endpoints](/2-4-2/data-flows/service-commissioning-files/resources/cybus-endpoint.md)).

## Connectware and Odoo Integration

Odoo exposes its business objects, such as manufacturing orders, through an external API that speaks JSON-RPC 2.0 instead of resource-oriented REST. Every call is an HTTP POST request to the single `/jsonrpc` path of the Odoo instance. The request body selects the model (for example, `mrp.production`), the method to call (for example, `search_read`), and the arguments. For details, see the [Odoo external API documentation](https://www.odoo.com/documentation/18.0/developer/reference/external_api.html).

Connectware communicates with this API through the [HTTP/REST connector](/2-4-2/connectors/enterprise-connectors/http-rest.md). Because all calls go to the same path, one write endpoint is enough. Each use case gets its own mapping that builds the matching JSON-RPC envelope with a `transform` rule and publishes it to that endpoint.

Two properties of the Odoo API shape this integration:

* **Authentication happens per request.** Every JSON-RPC call carries the database name, the numeric user ID, and the API key of the calling user in its body. The connection itself needs no credentials.
* **Every call is a POST request with a body.** The continuous polling of the HTTP/REST connector (`subscribe`) sends plain GET requests and cannot carry a JSON-RPC envelope. Reading data from Odoo is therefore triggered by an MQTT message instead of a timer.

{% hint style="info" %}
Odoo 19 introduces the External JSON-2 API as the successor of the JSON-RPC endpoint, and Odoo has announced the removal of the `/xmlrpc` and `/jsonrpc` endpoints for Odoo 22. This guide uses `/jsonrpc` because it is available across all currently supported Odoo versions. For details, see the [Odoo RPC API documentation](https://www.odoo.com/documentation/19.0/developer/reference/external_rpc_api.html).
{% endhint %}

The MQTT topics in this guide follow an ISA-95-style equipment hierarchy (`<enterprise>/<site>/<area>/<line>/<cell>`). The mappings subscribe with wildcards across all levels, so any machine in the hierarchy is picked up without changing the integration.

### Creating an API Key and Retrieving the User ID

JSON-RPC calls authenticate with an API key that takes the place of the user's password. To create one, log in to Odoo as the integration user, open the user profile, select the **Account Security** tab, and click **New API Key**. Copy the key and store it securely. Odoo does not display the key again.

The `execute_kw` RPC function identifies the user by a numeric user ID, not by the login name. You retrieve this ID once with an `authenticate` call, for example with `curl`:

{% code lineNumbers="true" %}

```bash
curl -s https://${ODOO_HOST}/jsonrpc \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "method": "call",
    "id": 1,
    "params": {
      "service": "common",
      "method": "authenticate",
      "args": ["${ODOO_DATABASE}", "${ODOO_LOGIN}", "${ODOO_API_KEY}", {}]
    }
  }'
```

{% endcode %}

Replace the placeholders with your values:

* `${ODOO_HOST}`: The host of your Odoo instance. For example, `mycompany.odoo.com`.
* `${ODOO_DATABASE}`: The name of the Odoo database. For Odoo Online instances, this is usually the subdomain of the host.
* `${ODOO_LOGIN}`: The login of the integration user, usually an email address.
* `${ODOO_API_KEY}`: The API key that you created for this user.

The response contains the numeric user ID in the `result` property, for example `{"jsonrpc": "2.0", "id": 1, "result": 7}`. If the credentials are wrong, `result` is `false`. Keep the user ID at hand, you set it as a service parameter in the next step.

### Odoo Connection Properties

We add the connection values as parameters to the service commissioning file, so you can set them when you install the service.

Do not worry about copying the service commissioning file snippets together into one, the complete example file is available at the end of this guide.

* `odooHost`: The host of your Odoo instance, without the scheme. For example, `mycompany.odoo.com`.
* `odooDatabase`: The name of the Odoo database.
* `odooUserId`: The numeric ID of the Odoo user that the integration acts as, retrieved with the `authenticate` call described in [Creating an API Key and Retrieving the User ID](#creating-an-api-key-and-retrieving-the-user-id).
* `odooApiKey`: The API key of that user.
* `topicRoot`: The root of the MQTT topic hierarchy. Defaults to `enterprise`.

{% code lineNumbers="true" %}

```yaml
parameters:
  odooHost:
    description: Host of your Odoo instance, without the scheme
    type: string
    default: mycompany.odoo.com

  odooDatabase:
    description: Name of the Odoo database
    type: string
    default: mycompany

  odooUserId:
    description: Numeric ID of the Odoo user that the integration acts as
    type: integer

  odooApiKey:
    description: API key of the Odoo user
    type: string

  topicRoot:
    description: Root of the MQTT topic hierarchy
    type: string
    default: enterprise
```

{% endcode %}

### Odoo Connection

To connect to the Odoo external API, we set up a `Cybus::Connection` resource that uses the HTTP/REST connector. The connection carries no credentials because every JSON-RPC request authenticates itself: the mappings insert the database name, the user ID, and the API key into the request body through parameter substitution.

{% code lineNumbers="true" %}

```yaml
resources:
  odooConnection:
    type: Cybus::Connection
    properties:
      protocol: Http
      connection:
        scheme: https
        host: !ref odooHost
        port: 443
```

{% endcode %}

All JSON-RPC calls share one write endpoint that posts to the `/jsonrpc` path. The HTTP/REST connector forwards the `body` property of every incoming message as the HTTP request body (see [Publishing Data to REST Servers](/2-4-2/connectors/enterprise-connectors/http-rest.md#publishing-data-to-rest-servers)).

{% code lineNumbers="true" %}

```yaml
odooJsonRpcEndpoint:
  type: Cybus::Endpoint
  properties:
    protocol: Http
    connection: !ref odooConnection
    write:
      path: /jsonrpc
```

{% endcode %}

Since both of the following mappings publish to this endpoint, Odoo's answers to both calls arrive on the same `/res` topic. Each mapping sets a distinct JSON-RPC `id` (`1` for reading manufacturing orders, `2` for logging notes), so you can tell the responses apart.

### Reading Manufacturing Orders

To read manufacturing orders, the integration calls the `search_read` method of the `mrp.production` model. The domain filter `["state", "in", ["confirmed", "progress"]]` restricts the result to orders that are confirmed or in progress, and the `fields` list limits the response to the reference, state, planned quantity, quantity currently in production, and start date.

The mapping listens on a trigger topic. Publishing any message to this topic, for example an empty JSON object, requests the current list of manufacturing orders. The trigger message content is ignored, the `transform` rule replaces it with the complete JSON-RPC envelope.

{% code lineNumbers="true" %}

```yaml
readManufacturingOrdersMapping:
  type: Cybus::Mapping
  properties:
    mappings:
      - subscribe:
          topic: !sub '${topicRoot}/odoo/manufacturing-orders/read'
        publish:
          endpoint: !ref odooJsonRpcEndpoint
        rules:
          - transform:
              expression: !sub |
                {
                  "body": {
                    "jsonrpc": "2.0",
                    "method": "call",
                    "id": 1,
                    "params": {
                      "service": "object",
                      "method": "execute_kw",
                      "args": [
                        "${odooDatabase}",
                        ${odooUserId},
                        "${odooApiKey}",
                        "mrp.production",
                        "search_read",
                        [[["state", "in", ["confirmed", "progress"]]]],
                        {
                          "fields": ["name", "state", "product_qty", "qty_producing", "date_start"],
                          "limit": 50
                        }
                      ]
                    }
                  }
                }
```

{% endcode %}

Odoo's answer is published to the `/res` topic of the endpoint. It contains the JSON-RPC response with one entry per manufacturing order. The `id` property of each entry is the database ID of the order, which you need to log notes on it:

{% code lineNumbers="true" %}

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {
      "id": 42,
      "name": "WH/MO/00042",
      "state": "progress",
      "product_qty": 100.0,
      "qty_producing": 25.0,
      "date_start": "2026-07-16 06:30:00"
    }
  ]
}
```

{% endcode %}

### Logging Production Events on Manufacturing Orders

For the write direction, the integration calls the `message_post` method of the `mrp.production` model. This appends a note to the log of the manufacturing order, where every Odoo user working with the order can see it. Machines publish their events to the topic hierarchy with a payload that names the order and the note text:

{% code lineNumbers="true" %}

```json
{
  "orderId": 42,
  "note": "Temperature deviation of 3.2 degrees Celsius on press-01"
}
```

{% endcode %}

* `orderId`: The database ID of the manufacturing order, as returned in the `id` field of the `search_read` result.
* `note`: The note text. HTML markup is allowed.

The `transform` rule builds the JSON-RPC envelope around these two values. The property name `body` appears twice with different meanings: the outer `body` is the HTTP request body that the HTTP/REST connector forwards to Odoo, and the inner `body` is the keyword argument of `message_post` that carries the note text.

{% code lineNumbers="true" %}

```yaml
logNoteMapping:
  type: Cybus::Mapping
  properties:
    mappings:
      - subscribe:
          topic: !sub '${topicRoot}/+site/+area/+line/+cell/production-events'
        publish:
          endpoint: !ref odooJsonRpcEndpoint
        rules:
          - transform:
              expression: !sub |
                {
                  "body": {
                    "jsonrpc": "2.0",
                    "method": "call",
                    "id": 2,
                    "params": {
                      "service": "object",
                      "method": "execute_kw",
                      "args": [
                        "${odooDatabase}",
                        ${odooUserId},
                        "${odooApiKey}",
                        "mrp.production",
                        "message_post",
                        [[orderId]],
                        { "body": note }
                      ]
                    }
                  }
                }
```

{% endcode %}

Any message published to a matching topic, for example `enterprise/hamburg/assembly/line-1/press-01/production-events`, now appears as a note on the manufacturing order in Odoo.

### Receiving Data from Odoo

For the return direction, Connectware hosts an HTTP route using the `Cybus::Server::Http` and `Cybus::Node::Http` resources. A mapping publishes every request body that arrives on this route to an MQTT topic, where any other Connectware service can pick it up.

{% code lineNumbers="true" %}

```yaml
odooWebhookServer:
  type: Cybus::Server::Http
  properties:
    basePath: /odoo

odooWebhookRoute:
  type: Cybus::Node::Http
  properties:
    parent: !ref odooWebhookServer
    method: POST
    route: /events

webhookMapping:
  type: Cybus::Mapping
  properties:
    mappings:
      - subscribe:
          endpoint: !ref odooWebhookRoute
        publish:
          topic: !sub '${topicRoot}/odoo/events'
```

{% endcode %}

The route is served under the `/data` prefix of your Connectware instance: `https://<connectware>/data/odoo/events`.

In Odoo 17.0 and later, automation rules provide the **Send Webhook Notification** action, which sends a POST request with the values of selected fields whenever the rule triggers. Create an automation rule on the Manufacturing Order model, for example triggered when the state changes to done, add a **Send Webhook Notification** action, set the URL to the Connectware route, and select the fields to send. For details, see the [Odoo webhooks documentation](https://www.odoo.com/documentation/18.0/applications/studio/automated_actions/webhooks.html).

The calling client must be authenticated and needs write permission on the path `data/odoo/events`. For more information, see [HTTP Server Permissions](/2-4-2/connectors/servers/http-server.md#permissions). The built-in webhook action does not add authentication headers to its requests. If your setup requires them, send the webhook from an Odoo server action instead.

## Verifying the Integration

1. Install the service and set the parameters with your Odoo host, database name, user ID, and API key.
2. Check that the connection is in the **Connected** state on the service details page in the Admin UI. The connection state only confirms that the Odoo host is reachable. A wrong database name, user ID, or API key only shows up in the response of a call.
3. Publish an empty JSON object to `enterprise/odoo/manufacturing-orders/read`, for example with an MQTT client or the Admin UI. Use the [Data Explorer](/2-4-2/monitoring/data-explorer.md) to inspect the `/res` topic of the endpoint. The response with the JSON-RPC id `1` contains the list of manufacturing orders.
4. Pick the `id` of one manufacturing order from the response and publish a test event to `enterprise/hamburg/assembly/line-1/press-01/production-events` with this ID as `orderId`. Open the manufacturing order in Odoo and check that the note appears in its log.
5. Trigger the automation rule in Odoo, for example by marking a manufacturing order as done, and check that the message arrives on `enterprise/odoo/events`.

Odoo answers application errors, such as wrong credentials or unknown field names, with HTTP status 200 and an `error` object inside the JSON-RPC response. If a call has no effect, inspect the messages on the `/res` topic: application errors appear as an `error` object inside the JSON-RPC response, while network errors appear as an `error` property of the result message itself.

## Service Commissioning File Example

{% file src="/files/ZX7qNRnmvXqLQJdEFblQ" %}

{% code title="odoo-example.yml" lineNumbers="true" expandable="true" %}

```yaml
---
# ----------------------------------------------------------------------------
# Service Commissioning File
# ----------------------------------------------------------------------------
# Copyright: Cybus GmbH
# Contact: support@cybus.io
# ----------------------------------------------------------------------------
# Odoo Integration (Example)
# ----------------------------------------------------------------------------

description: >
  Service commissioning file for the bidirectional integration between
  Connectware and Odoo (Example)

metadata:
  name: Odoo Integration
  provider: cybus
  homepage: https://www.cybus.io
  version: 1.0.0

parameters:
  odooHost:
    description: Host of your Odoo instance, without the scheme
    type: string
    default: mycompany.odoo.com

  odooDatabase:
    description: Name of the Odoo database
    type: string
    default: mycompany

  odooUserId:
    description: Numeric ID of the Odoo user that the integration acts as
    type: integer

  odooApiKey:
    description: API key of the Odoo user
    type: string

  topicRoot:
    description: Root of the MQTT topic hierarchy
    type: string
    default: enterprise

resources:
  # Connection to the Odoo external API. Authentication happens per request
  # inside the JSON-RPC envelope, so the connection carries no credentials.
  odooConnection:
    type: Cybus::Connection
    properties:
      protocol: Http
      connection:
        scheme: https
        host: !ref odooHost
        port: 443

  # Single write endpoint for all JSON-RPC calls
  odooJsonRpcEndpoint:
    type: Cybus::Endpoint
    properties:
      protocol: Http
      connection: !ref odooConnection
      write:
        path: /jsonrpc

  # Reads open manufacturing orders whenever a message arrives on the
  # trigger topic. The JSON-RPC id 1 marks the responses on the /res topic.
  readManufacturingOrdersMapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            topic: !sub '${topicRoot}/odoo/manufacturing-orders/read'
          publish:
            endpoint: !ref odooJsonRpcEndpoint
          rules:
            - transform:
                expression: !sub |
                  {
                    "body": {
                      "jsonrpc": "2.0",
                      "method": "call",
                      "id": 1,
                      "params": {
                        "service": "object",
                        "method": "execute_kw",
                        "args": [
                          "${odooDatabase}",
                          ${odooUserId},
                          "${odooApiKey}",
                          "mrp.production",
                          "search_read",
                          [[["state", "in", ["confirmed", "progress"]]]],
                          {
                            "fields": ["name", "state", "product_qty", "qty_producing", "date_start"],
                            "limit": 50
                          }
                        ]
                      }
                    }
                  }

  # Logs shop floor events as notes on Odoo manufacturing orders.
  # The JSON-RPC id 2 marks the responses on the /res topic.
  logNoteMapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            topic: !sub '${topicRoot}/+site/+area/+line/+cell/production-events'
          publish:
            endpoint: !ref odooJsonRpcEndpoint
          rules:
            - transform:
                expression: !sub |
                  {
                    "body": {
                      "jsonrpc": "2.0",
                      "method": "call",
                      "id": 2,
                      "params": {
                        "service": "object",
                        "method": "execute_kw",
                        "args": [
                          "${odooDatabase}",
                          ${odooUserId},
                          "${odooApiKey}",
                          "mrp.production",
                          "message_post",
                          [[orderId]],
                          { "body": note }
                        ]
                      }
                    }
                  }

  # HTTP route that Odoo automation rules can call
  odooWebhookServer:
    type: Cybus::Server::Http
    properties:
      basePath: /odoo

  odooWebhookRoute:
    type: Cybus::Node::Http
    properties:
      parent: !ref odooWebhookServer
      method: POST
      route: /events

  webhookMapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            endpoint: !ref odooWebhookRoute
          publish:
            topic: !sub '${topicRoot}/odoo/events'
```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.cybus.io/2-4-2/guides/system-connectivity/mes-erp-business-systems/odoo-integration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
