Making External HTTP API Calls from Odoo Models#
Why Call External APIs from Odoo?#
Odoo models sometimes need to push or pull data from external systems: shipping carriers, payment gateways, ERP connectors, weather APIs, or custom SaaS backends. Odoo ships with the requests library available in its Python environment.
Basic HTTP GET#
import requests
from odoo import models
class ShipmentOrder(models.Model):
_name = 'shipment.order'
def fetch_tracking_status(self):
api_key = self.env['ir.config_parameter'].sudo().get_param(
'my_module.carrier_api_key'
)
url = f"https://api.carrier.com/track/{self.tracking_number}"
response = requests.get(
url,
headers={'Authorization': f'Bearer {api_key}'},
timeout=10,
)
response.raise_for_status() # raises HTTPError for 4xx/5xx
data = response.json()
self.tracking_status = data.get('status', 'unknown')Basic HTTP POST with JSON Body#
def create_remote_invoice(self):
payload = {
'partner_id': self.partner_id.name,
'amount': float(self.amount_total),
'currency': self.currency_id.name,
}
response = requests.post(
'https://api.billing.com/invoices',
json=payload,
headers={'X-Api-Key': self._get_api_key()},
timeout=15,
)
response.raise_for_status()
return response.json()Storing API Keys Securely#
Never hardcode API keys in Python. Use ir.config_parameter:
# Read key
key = self.env['ir.config_parameter'].sudo().get_param('my_module.api_key')
# Write key (e.g. from a wizard)
self.env['ir.config_parameter'].sudo().set_param('my_module.api_key', value)Expose the setting in Settings → Technical → Parameters → System Parameters, or add a settings form section:
<record id="action_my_module_settings" model="ir.actions.act_window">
...
</record>Error Handling#
from requests.exceptions import RequestException, Timeout, HTTPError
from odoo.exceptions import UserError
def sync_with_external(self):
try:
response = requests.post(
self._endpoint_url(),
json=self._build_payload(),
timeout=20,
)
response.raise_for_status()
except Timeout:
raise UserError("External API timed out. Try again later.")
except HTTPError as e:
raise UserError(f"API error {e.response.status_code}: {e.response.text[:200]}")
except RequestException as e:
raise UserError(f"Network error: {e}")Avoiding Blocking the Main Thread#
Long-running API calls block the Odoo worker process and slow down all users sharing that worker. Use these strategies:
1. Async via ir.cron (polling pattern):
Schedule a cron job that processes a queue table instead of calling the API in the user's request:
class ApiCallQueue(models.Model):
_name = 'api.call.queue'
_description = 'Async API Call Queue'
state = fields.Selection([('pending', 'Pending'), ('done', 'Done'), ('error', 'Error')])
payload = fields.Json()
response = fields.Json()
error_message = fields.Char()
def action_process_queue(self):
pending = self.search([('state', '=', 'pending')], limit=50)
for rec in pending:
try:
resp = requests.post(rec._endpoint(), json=rec.payload, timeout=15)
resp.raise_for_status()
rec.write({'state': 'done', 'response': resp.json()})
except Exception as e:
rec.write({'state': 'error', 'error_message': str(e)})2. Webhook receiver (push pattern):
Register a controller to receive API callbacks instead of polling:
from odoo import http
from odoo.http import request
class WebhookController(http.Controller):
@http.route('/webhook/carrier/tracking', type='json', auth='none', csrf=False)
def carrier_tracking_webhook(self, **kwargs):
data = request.get_json_data()
token = request.httprequest.headers.get('X-Webhook-Token', '')
expected = request.env['ir.config_parameter'].sudo().get_param(
'my_module.webhook_secret'
)
if token != expected:
return {'status': 'unauthorized'}
# Process the webhook payload
tracking_no = data.get('tracking_number')
order = request.env['shipment.order'].sudo().search(
[('tracking_number', '=', tracking_no)], limit=1
)
if order:
order.tracking_status = data.get('status')
return {'status': 'ok'}Session Reuse for Performance#
If you make many calls to the same host, reuse a requests.Session:
class ExternalSync(models.AbstractModel):
_name = 'external.sync.mixin'
def _get_session(self):
session = requests.Session()
session.headers.update({'Authorization': f'Bearer {self._get_api_key()}'})
session.mount('https://', requests.adapters.HTTPAdapter(max_retries=2))
return session
def bulk_sync(self, records):
with self._get_session() as session:
for rec in records:
resp = session.post(self._endpoint(), json=rec._payload(), timeout=10)
resp.raise_for_status()Common Mistakes#
- No timeout -
requests.get(url)with no timeout can hang indefinitely, consuming a worker slot - Storing API keys in Python - use
ir.config_parameterso keys are configurable without code changes - Calling external APIs on record write - triggers block the save transaction; use a queue or cron instead
- Ignoring HTTP errors - always call
response.raise_for_status()and catchHTTPError - Logging full response bodies - responses may contain PII or secrets; log only status codes and sanitized metadata

