API REST Odoo: exemplos práticos e tutorial de integração
De acordo com o relatório State of APIs de 2025 do Postman, 73% das empresas agora integram seu ERP com pelo menos três sistemas externos. Odoo, que atende mais de 12 milhões de usuários em todo o mundo, expõe todo o seu modelo de dados por meio de uma rica camada de API. No entanto, a documentação deixa muitos desenvolvedores lutando com fluxos de autenticação, operações em lote e tratamento de erros em nível de produção.
Este tutorial fornece exemplos prontos para copiar e colar em Python e Node.js para cada padrão de integração comum. Esteja você sincronizando pedidos do Shopify, enviando dados de um aplicativo móvel ou criando um painel personalizado, este guia irá ajudá-lo.
Principais conclusões
- Odoo oferece três métodos de acesso à API: XML-RPC (legado, todas as versões), JSON-RPC (protocolo de cliente web) e API REST (Odoo 17+ com chaves de API) — cada um com autenticação e casos de uso diferentes.
- Autenticação de chave de API (Odoo 17+) é a abordagem recomendada para integrações de servidor para servidor — sem gerenciamento de sessão, sem tokens CSRF, cabeçalhos HTTP diretos.
- Domínios de pesquisa usam a poderosa notação polonesa do Odoo para filtragem. Domine os operadores e você poderá consultar qualquer combinação de dados.
- Operações em lote são essenciais para o desempenho: a criação de 1.000 registros com uma chamada de API é 50 vezes mais rápida do que 1.000 chamadas individuais.
- O tratamento de erros e a limitação de taxa são essenciais para integrações de produção — Odoo retorna respostas de erro estruturadas que seu código deve analisar e tratar normalmente.
1. Métodos de autenticação
Método 1: Chaves de API (recomendado para Odoo 17+)
As chaves de API são o método mais simples e seguro para comunicação entre servidores:
# Generate an API key in Odoo:
# Settings → Users → Select user → Account Security → New API Key
# Test authentication
curl -X GET "https://your-odoo.com/api/res.partner" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Python com chave de API:
import requests
class OdooAPI:
def __init__(self, url, api_key):
self.url = url.rstrip('/')
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json',
})
def get(self, model, params=None):
response = self.session.get(
f'{self.url}/api/{model}',
params=params or {}
)
response.raise_for_status()
return response.json()
def post(self, model, data):
response = self.session.post(
f'{self.url}/api/{model}',
json=data
)
response.raise_for_status()
return response.json()
# Usage
odoo = OdooAPI('https://your-odoo.com', 'your-api-key-here')
partners = odoo.get('res.partner', {'limit': 10})
Node.js com chave de API:
const axios = require('axios');
class OdooAPI {
constructor(url, apiKey) {
this.client = axios.create({
baseURL: `${url}/api`,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
timeout: 30000,
});
}
async get(model, params = {}) {
const { data } = await this.client.get(`/${model}`, { params });
return data;
}
async post(model, body) {
const { data } = await this.client.post(`/${model}`, body);
return data;
}
async put(model, id, body) {
const { data } = await this.client.put(`/${model}/${id}`, body);
return data;
}
async delete(model, id) {
const { data } = await this.client.delete(`/${model}/${id}`);
return data;
}
}
// Usage
const odoo = new OdooAPI('https://your-odoo.com', 'your-api-key');
const partners = await odoo.get('res.partner', { limit: 10 });
Método 2: JSON-RPC (todas as versões)
JSON-RPC é o protocolo usado internamente pelo cliente web do Odoo. Funciona com todas as versões do Odoo:
import requests
import json
class OdooJsonRpc:
def __init__(self, url, db, username, password):
self.url = url.rstrip('/')
self.db = db
self.session = requests.Session()
self.uid = self._authenticate(username, password)
def _authenticate(self, username, password):
response = self._call('/web/session/authenticate', {
'db': self.db,
'login': username,
'password': password,
})
if not response.get('uid'):
raise Exception(f"Authentication failed: {response}")
return response['uid']
def _call(self, endpoint, params):
payload = {
'jsonrpc': '2.0',
'method': 'call',
'params': params,
'id': None,
}
response = self.session.post(
f'{self.url}{endpoint}',
json=payload,
headers={'Content-Type': 'application/json'}
)
result = response.json()
if result.get('error'):
raise Exception(result['error']['data']['message'])
return result.get('result')
def search_read(self, model, domain=None, fields=None, limit=80, offset=0, order=None):
return self._call('/web/dataset/call_kw', {
'model': model,
'method': 'search_read',
'args': [domain or []],
'kwargs': {
'fields': fields or [],
'limit': limit,
'offset': offset,
'order': order or '',
},
})
def create(self, model, values):
return self._call('/web/dataset/call_kw', {
'model': model,
'method': 'create',
'args': [values],
'kwargs': {},
})
def write(self, model, ids, values):
return self._call('/web/dataset/call_kw', {
'model': model,
'method': 'write',
'args': [ids, values],
'kwargs': {},
})
# Usage
odoo = OdooJsonRpc('https://your-odoo.com', 'mydb', 'admin', 'password')
orders = odoo.search_read('sale.order', [('state', '=', 'sale')],
fields=['name', 'amount_total', 'partner_id'],
limit=20)
Método 3: XML-RPC (Legado, Universal)
import xmlrpc.client
url = 'https://your-odoo.com'
db = 'mydb'
username = 'admin'
password = 'password'
# Authenticate
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
uid = common.authenticate(db, username, password, {})
# Create models proxy
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
# Search and read
partners = models.execute_kw(db, uid, password,
'res.partner', 'search_read',
[[('is_company', '=', True), ('country_id.code', '=', 'US')]],
{'fields': ['name', 'email', 'phone'], 'limit': 10}
)
2. Operações CRUD
Criar registros
# Create a single contact
partner_id = odoo.create('res.partner', {
'name': 'Acme Corporation',
'is_company': True,
'email': '[email protected]',
'phone': '+1-555-0123',
'street': '123 Main Street',
'city': 'San Francisco',
'state_id': 5, # California
'country_id': 233, # United States
'category_id': [(6, 0, [1, 3])], # Tags: replace all with IDs 1 and 3
})
# Create a sale order with lines
order_id = odoo.create('sale.order', {
'partner_id': partner_id,
'date_order': '2026-03-23',
'order_line': [
(0, 0, {
'product_id': 42,
'product_uom_qty': 5,
'price_unit': 99.99,
}),
(0, 0, {
'product_id': 43,
'product_uom_qty': 2,
'price_unit': 149.99,
}),
],
})
Equivalente em Node.js:
// Create a contact
const partnerId = await odoo.post('res.partner', {
name: 'Acme Corporation',
is_company: true,
email: '[email protected]',
phone: '+1-555-0123',
street: '123 Main Street',
city: 'San Francisco',
country_id: 233,
});
// Create sale order with lines
const orderId = await odoo.post('sale.order', {
partner_id: partnerId,
date_order: '2026-03-23',
order_line: [
[0, 0, { product_id: 42, product_uom_qty: 5, price_unit: 99.99 }],
[0, 0, { product_id: 43, product_uom_qty: 2, price_unit: 149.99 }],
],
});
Ler registros
# Read specific fields from specific records
data = odoo.search_read('sale.order',
domain=[('state', '=', 'sale'), ('amount_total', '>', 500)],
fields=['name', 'partner_id', 'amount_total', 'date_order', 'state'],
limit=50,
offset=0,
order='date_order desc'
)
# Read a single record by ID (REST API)
# GET /api/sale.order/42?fields=name,amount_total
// Node.js — read with pagination
async function fetchAllOrders(odoo) {
const pageSize = 100;
let offset = 0;
let allOrders = [];
while (true) {
const orders = await odoo.get('sale.order', {
domain: JSON.stringify([['state', '=', 'sale']]),
fields: 'name,partner_id,amount_total',
limit: pageSize,
offset,
order: 'date_order desc',
});
allOrders = allOrders.concat(orders);
if (orders.length < pageSize) break;
offset += pageSize;
}
return allOrders;
}
Atualizar registros
# Update a single record
odoo.write('res.partner', [partner_id], {
'phone': '+1-555-9999',
'website': 'https://acme.com',
})
# Update multiple records at once
draft_orders = odoo.search_read('sale.order',
[('state', '=', 'draft'), ('date_order', '<', '2026-01-01')],
fields=['id']
)
ids = [o['id'] for o in draft_orders]
odoo.write('sale.order', ids, {'note': 'Reviewed Q1 2026'})
Excluir registros
# Delete records (use with caution)
odoo.write('res.partner', [obsolete_id], {'active': False}) # Prefer archiving
# Actually delete (rarely needed)
# models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[obsolete_id]])
3. Domínios de pesquisa avançada
Os domínios de pesquisa do Odoo usam notação polonesa (notação de prefixo) para combinar condições. O operador padrão entre condições é AND. Use barra vertical '|' para OR e e comercial '&' para AND explícito. Cada folha é uma tupla de nome de campo, operador e valor. Odoo suporta notação de ponto para filtrar campos de modelo relacionados, como 'partner_id.country_id.code' para filtrar pedidos por país do cliente.
# Complex domain examples
# Orders from US customers with amount > $1000, created this year
domain = [
('partner_id.country_id.code', '=', 'US'),
('amount_total', '>', 1000),
('date_order', '>=', '2026-01-01'),
('state', 'in', ['sale', 'done']),
]
# OR condition: email contains 'gmail' OR 'yahoo'
domain = [
'|',
('email', 'ilike', 'gmail.com'),
('email', 'ilike', 'yahoo.com'),
]
# Complex: (state=sale AND amount>1000) OR (state=done AND amount>5000)
domain = [
'|',
'&', ('state', '=', 'sale'), ('amount_total', '>', 1000),
'&', ('state', '=', 'done'), ('amount_total', '>', 5000),
]
# Negation: NOT archived
domain = [('active', '!=', False)]
# NULL check: has no email
domain = [('email', '=', False)]
# Hierarchical: all child categories of 'Electronics'
domain = [('categ_id', 'child_of', electronics_id)]
# Date ranges
from datetime import datetime, timedelta
domain = [
('create_date', '>=', (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')),
('create_date', '<', datetime.now().strftime('%Y-%m-%d')),
]
Referência do Operador
| Operador | Descrição | Exemplo |
|---|---|---|
| CÓDIGO0 | Correspondência exata | CÓDIGO1 |
| CÓDIGO0 | Diferente | CÓDIGO1 |
>, >=, <, <= | Comparação | CÓDIGO4 |
| CÓDIGO0 | Valor na lista | CÓDIGO1 |
| CÓDIGO0 | Valor não listado | CÓDIGO1 |
| CÓDIGO0 | SQL LIKE (diferencia maiúsculas de minúsculas) | CÓDIGO1 |
| CÓDIGO0 | LIKE sem distinção entre maiúsculas e minúsculas | CÓDIGO1 |
| CÓDIGO0 | Correspondência de padrões | CÓDIGO1 |
| CÓDIGO0 | Descendentes hierárquicos | CÓDIGO1 |
| CÓDIGO0 | Ancestrais hierárquicos | CÓDIGO1 |
4. Operações em lote
As operações em lote são essenciais para o desempenho. Nunca crie registros um de cada vez em um loop:
# BAD: 1000 API calls (slow, ~300 seconds)
for customer in customers:
odoo.create('res.partner', customer)
# GOOD: 1 API call with batch (fast, ~3 seconds)
# Using JSON-RPC batch create
partner_ids = odoo._call('/web/dataset/call_kw', {
'model': 'res.partner',
'method': 'create',
'args': [customers], # Pass list of dicts
'kwargs': {},
})
// Node.js batch pattern with chunking
async function batchCreate(odoo, model, records, chunkSize = 200) {
const results = [];
for (let i = 0; i < records.length; i += chunkSize) {
const chunk = records.slice(i, i + chunkSize);
console.log(`Creating ${model} batch ${i / chunkSize + 1}/${Math.ceil(records.length / chunkSize)}`);
const ids = await odoo.post(model, chunk);
results.push(...(Array.isArray(ids) ? ids : [ids]));
// Respect rate limits
if (i + chunkSize < records.length) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
return results;
}
// Usage
const customers = generateCustomerData(); // Array of 5000 objects
const ids = await batchCreate(odoo, 'res.partner', customers);
console.log(`Created ${ids.length} partners`);
5. Tratamento de erros
As integrações de produção devem lidar com erros normalmente:
import requests
import logging
import time
logger = logging.getLogger(__name__)
class OdooAPIClient:
MAX_RETRIES = 3
RETRY_DELAY = 2 # seconds
def __init__(self, url, api_key):
self.url = url.rstrip('/')
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json',
})
def _request(self, method, endpoint, **kwargs):
last_error = None
for attempt in range(self.MAX_RETRIES):
try:
response = self.session.request(
method,
f'{self.url}{endpoint}',
timeout=30,
**kwargs
)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
logger.warning(f"Rate limited. Retrying after {retry_after}s")
time.sleep(retry_after)
continue
if response.status_code == 401:
raise AuthenticationError("Invalid API key or session expired")
if response.status_code == 403:
raise PermissionError(f"Access denied: {response.text}")
if response.status_code == 404:
raise RecordNotFoundError(f"Record not found: {endpoint}")
if response.status_code >= 500:
logger.error(f"Server error {response.status_code}: {response.text}")
if attempt < self.MAX_RETRIES - 1:
time.sleep(self.RETRY_DELAY * (attempt + 1))
continue
raise ServerError(f"Server error after {self.MAX_RETRIES} retries")
response.raise_for_status()
return response.json()
except requests.exceptions.ConnectionError as e:
last_error = e
logger.warning(f"Connection error (attempt {attempt + 1}): {e}")
if attempt < self.MAX_RETRIES - 1:
time.sleep(self.RETRY_DELAY * (attempt + 1))
except requests.exceptions.Timeout as e:
last_error = e
logger.warning(f"Request timeout (attempt {attempt + 1}): {e}")
if attempt < self.MAX_RETRIES - 1:
time.sleep(self.RETRY_DELAY * (attempt + 1))
raise ConnectionError(f"Failed after {self.MAX_RETRIES} attempts: {last_error}")
class AuthenticationError(Exception): pass
class PermissionError(Exception): pass
class RecordNotFoundError(Exception): pass
class ServerError(Exception): pass
// Node.js error handling with axios interceptors
const axios = require('axios');
function createOdooClient(url, apiKey) {
const client = axios.create({
baseURL: `${url}/api`,
headers: { Authorization: `Bearer ${apiKey}` },
timeout: 30000,
});
// Response interceptor for error handling
client.interceptors.response.use(
(response) => response,
async (error) => {
const { config, response } = error;
config.__retryCount = config.__retryCount || 0;
// Rate limiting
if (response?.status === 429) {
const retryAfter = parseInt(response.headers['retry-after'] || '60', 10);
console.warn(`Rate limited. Waiting ${retryAfter}s...`);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
return client(config);
}
// Retry on server errors (max 3)
if (response?.status >= 500 && config.__retryCount < 3) {
config.__retryCount += 1;
const delay = config.__retryCount * 2000;
console.warn(`Server error ${response.status}. Retry ${config.__retryCount}/3 in ${delay}ms`);
await new Promise((r) => setTimeout(r, delay));
return client(config);
}
// Structured error response
const errorMessage = response?.data?.error?.message
|| response?.data?.message
|| error.message;
throw new Error(`Odoo API Error [${response?.status}]: ${errorMessage}`);
}
);
return client;
}
6. Exemplos de integração no mundo real
Sincronização de pedidos do Shopify para Odoo
class ShopifyOdooSync:
def __init__(self, odoo_client, shopify_client):
self.odoo = odoo_client
self.shopify = shopify_client
def sync_order(self, shopify_order):
# 1. Find or create customer
partner = self._get_or_create_partner(shopify_order['customer'])
# 2. Map products
order_lines = []
for item in shopify_order['line_items']:
product_id = self._find_product_by_sku(item['sku'])
if not product_id:
logger.warning(f"Product not found for SKU: {item['sku']}")
continue
order_lines.append((0, 0, {
'product_id': product_id,
'product_uom_qty': item['quantity'],
'price_unit': float(item['price']),
'discount': self._calc_discount(item),
}))
# 3. Create sale order
order_id = self.odoo.create('sale.order', {
'partner_id': partner['id'],
'client_order_ref': shopify_order['name'], # Shopify order #
'order_line': order_lines,
'note': f"Shopify Order: {shopify_order['id']}",
})
# 4. Auto-confirm if paid
if shopify_order['financial_status'] == 'paid':
self.odoo._call('/web/dataset/call_kw', {
'model': 'sale.order',
'method': 'action_confirm',
'args': [[order_id]],
'kwargs': {},
})
return order_id
def _get_or_create_partner(self, customer):
# Search by email first
existing = self.odoo.search_read('res.partner',
[('email', '=', customer['email'])],
fields=['id', 'name'], limit=1)
if existing:
return existing[0]
return {'id': self.odoo.create('res.partner', {
'name': f"{customer['first_name']} {customer['last_name']}",
'email': customer['email'],
'phone': customer.get('phone'),
})}
def _find_product_by_sku(self, sku):
products = self.odoo.search_read('product.product',
[('default_code', '=', sku)],
fields=['id'], limit=1)
return products[0]['id'] if products else None
Receptor Webhook (exemplo de frasco)
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
@app.route('/webhook/odoo/order', methods=['POST'])
def handle_odoo_webhook():
# Verify signature
signature = request.headers.get('X-Odoo-Signature')
expected = hmac.new(
WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return jsonify({'error': 'Invalid signature'}), 401
payload = request.json
event_type = payload.get('event')
if event_type == 'sale.order.confirmed':
handle_order_confirmed(payload['data'])
elif event_type == 'stock.picking.done':
handle_shipment_complete(payload['data'])
return jsonify({'status': 'ok'}), 200
7. Dicas de desempenho
| Dica | Impacto | Detalhes |
|---|---|---|
Use o parâmetro fields | Alto | Solicite apenas os campos necessários - reduz a carga útil de 5 a 10 vezes |
| Criações em lote | Alto | 1 chamada com 500 registros versus 500 chamadas — 50x mais rápido |
| Paginar grandes conjuntos de dados | Médio | Use limit e offset — evite carregar 100 mil registros |
| Dados somente leitura em cache | Médio | Cache de catálogos de produtos, categorias (TTL 5-15 min) |
Usar search_count | Baixo | Conte antes de buscar — evite carregar dados apenas para contar |
| Pool de conexões | Médio | Reutilize sessões HTTP — economiza sobrecarga de handshake TLS |
Perguntas frequentes
Qual é a diferença entre XML-RPC, JSON-RPC e API REST no Odoo?
XML-RPC é o protocolo legado disponível em todas as versões do Odoo — é detalhado, mas tem suporte universal. JSON-RPC é o protocolo usado pelo cliente web do Odoo e fornece a mesma funcionalidade com cargas JSON. A API REST foi introduzida no Odoo 17 e fornece endpoints HTTP padrão com autenticação de chave de API, tornando-a a opção mais fácil para integrações modernas. Para novos projetos, use a API REST se você estiver no Odoo 17 ou posterior.
Como faço para lidar com os limites de taxa da API Odoo?
Odoo.sh aplica limites de taxa com base no nível do seu plano. Ao receber uma resposta 429, leia o cabeçalho Retry-After e aguarde antes de tentar novamente. Para integrações de alto volume, implemente a espera exponencial, agrupe suas operações para reduzir o número de chamadas de API e considere usar as ações programadas do Odoo para processamento em massa, em vez de chamadas de API em tempo real para sincronizações não críticas.
Posso chamar métodos Python personalizados por meio da API?
Sim. Qualquer método público em um modelo Odoo pode ser chamado através de XML-RPC ou JSON-RPC usando execute_kw. Para a API REST, você precisa criar um endpoint de controlador personalizado com @http.route. Os métodos que começam com sublinhado são privados e não podem ser chamados externamente através de XML-RPC. Sempre valide as entradas em seus métodos personalizados para evitar ataques de injeção.
Como sincronizo grandes conjuntos de dados com eficiência?
Use uma combinação de estratégias: sincronização completa inicial com operações em lote e paginação (limite de 200 registros por solicitação) e, em seguida, sincronizações incrementais usando a filtragem write_date para buscar apenas os registros modificados desde a última sincronização. Armazene o carimbo de data/hora da última sincronização e use-o como filtro de domínio. Para conjuntos de dados muito grandes que excedem 100.000 registros, considere a replicação direta do banco de dados em vez da sincronização por API.
A API REST do Odoo está disponível no Odoo Community Edition?
A API REST nativa com autenticação de chave de API foi introduzida no Odoo 17 Enterprise. Para a comunidade Odoo, você pode usar XML-RPC ou JSON-RPC, que estão disponíveis em todas as edições, ou instalar módulos da comunidade, como o rest-framework do OCA, que adiciona endpoints RESTful. Os serviços de integração do ECOSIRE suportam todas as edições Odoo e protocolos API.
Como lidar com os campos Many2many e One2many em chamadas de API?
Os campos relacionais usam tuplas de comandos especiais: (0, 0, valores) para criar e vincular um novo registro, (1, id, valores) para atualizar um registro vinculado, (2, id, 0) para excluir um registro vinculado, (3, id, 0) para desvincular sem excluir, (4, id, 0) para vincular um registro existente, (5, 0, 0) para desvincular todos e (6, 0, [ids]) para substituir todos os links. Para leitura, esses campos retornam listas de IDs por padrão. Use search_read com o nome do campo para obter dados completos.
Próximas etapas
A integração de API é a espinha dorsal dos sistemas empresariais modernos. Esteja você construindo uma sincronização de dados simples ou uma orquestração multiplataforma complexa, os padrões neste guia serão úteis para você.
Recursos relacionados:
- Guia de desenvolvimento Odoo Python — Aprofunde-se no backend Python do Odoo
- Guia de depuração de webhook — Solução de problemas de integrações de webhook
- ECOSIRE Marketplace Connectors — Integrações pré-construídas para as principais plataformas
Precisa de ajuda com a integração da API Odoo? A equipe de integração da ECOSIRE conectou o Odoo a mais de 50 plataformas externas, incluindo Shopify, Amazon, Salesforce e ERPs personalizados. Desde simples sincronizações de dados até orquestração bidirecional em tempo real, construímos integrações escalonáveis. Agende uma consulta técnica gratuita.
Escrito por
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
Artigos Relacionados
Segmentação de clientes baseada em IA: do RFM ao clustering preditivo
Saiba como a IA transforma a segmentação de clientes, desde a análise estática de RFM até o clustering preditivo dinâmico. Guia de implementação com dados Python, Odoo e ROI real.
IA para otimização da cadeia de suprimentos: visibilidade, previsão e automação
Transforme as operações da cadeia de suprimentos com IA: detecção de demanda, pontuação de risco de fornecedores, otimização de rotas, automação de armazéns e previsão de interrupções. Guia 2026.
Padrões de integração de API: práticas recomendadas de arquitetura empresarial
Padrões de integração de API mestres para sistemas corporativos. REST vs GraphQL vs gRPC, arquitetura orientada a eventos, padrão saga, gateway de API e guia de controle de versão.