API REST de Odoo: ejemplos prácticos y tutorial de integración
Según el informe Estado de las API 2025 de Postman, el 73% de las empresas ahora integran su ERP con al menos tres sistemas externos. Odoo, que cuenta con más de 12 millones de usuarios en todo el mundo, expone todo su modelo de datos a través de una rica capa API. Sin embargo, la documentación deja a muchos desarrolladores luchando con los flujos de autenticación, las operaciones por lotes y el manejo de errores a nivel de producción.
Este tutorial proporciona ejemplos listos para copiar y pegar tanto en Python como en Node.js para cada patrón de integración común. Ya sea que esté sincronizando pedidos de Shopify, enviando datos desde una aplicación móvil o creando un panel personalizado, esta guía lo tiene cubierto.
Conclusiones clave
- Odoo ofrece tres métodos de acceso API: XML-RPC (heredado, todas las versiones), JSON-RPC (protocolo de cliente web) y REST API (Odoo 17+ con claves API), cada uno con diferentes casos de uso y autenticación.
- Autenticación de clave API (Odoo 17+) es el enfoque recomendado para integraciones de servidor a servidor: sin administración de sesiones, sin tokens CSRF, encabezados HTTP sencillos.
- Dominios de búsqueda utilizan la poderosa notación polaca de Odoo para filtrar: domine los operadores y podrá consultar cualquier combinación de datos.
- Las operaciones por lotes son fundamentales para el rendimiento: crear 1000 registros con una llamada API es 50 veces más rápido que 1000 llamadas individuales.
- El manejo de errores y la limitación de velocidad son esenciales para las integraciones de producción: Odoo devuelve respuestas de error estructuradas que su código debe analizar y manejar correctamente.
1. Métodos de autenticación
Método 1: Claves API (recomendado para Odoo 17+)
Las claves API son el método más simple y seguro para la comunicación de servidor a servidor:
# 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 con clave 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 con clave 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 las versiones)
JSON-RPC es el protocolo utilizado internamente por el cliente web de Odoo. Funciona con todas las versiones de 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 (heredado, 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. Operaciones CRUD
Crear 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 de 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 }],
],
});
Leer 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;
}
Actualizar 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'})
Eliminar 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. Dominios de búsqueda avanzada
Los dominios de búsqueda de Odoo utilizan notación polaca (notación de prefijo) para combinar condiciones. El operador predeterminado entre condiciones es AND. Utilice la tubería '|' para OR y el signo '&' para AND explícito. Cada hoja es una tupla de nombre de campo, operador y valor. Odoo admite la notación de puntos para filtrar campos de modelos relacionados, como 'partner_id.country_id.code' para filtrar pedidos por país del 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')),
]
Referencia del operador
| Operador | Descripción | Ejemplo |
|---|---|---|
| CÓDIGO0 | Coincidencia exacta | CÓDIGO1 |
| CÓDIGO0 | No igual | CÓDIGO1 |
| CÓDIGO0, CÓDIGO1, CÓDIGO2, CÓDIGO3 | Comparación | CÓDIGO4 |
| CÓDIGO0 | Valor en lista | CÓDIGO1 |
| CÓDIGO0 | Valor no en la lista | CÓDIGO1 |
| CÓDIGO0 | SQL LIKE (distingue entre mayúsculas y minúsculas) | CÓDIGO1 |
| CÓDIGO0 | Me gusta que no distingue entre mayúsculas y minúsculas | CÓDIGO1 |
| CÓDIGO0 | Coincidencia de patrones | CÓDIGO1 |
| CÓDIGO0 | Descendientes jerárquicos | CÓDIGO1 |
| CÓDIGO0 | Ancestros jerárquicos | CÓDIGO1 |
4. Operaciones por lotes
Las operaciones por lotes son esenciales para el rendimiento. Nunca cree registros uno a la vez en un bucle:
# 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. Manejo de errores
Las integraciones de producción deben manejar los errores con elegancia:
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. Ejemplos de integración del mundo real
Sincronización de pedidos de Shopify a 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 de webhook (ejemplo de matraz)
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. Consejos de rendimiento
| Consejo | Impacto | Detalles |
|---|---|---|
Utilice el parámetro fields | Alto | Solicite solo los campos necesarios: reduce la carga útil entre 5 y 10 veces |
| Creación por lotes | Alto | 1 llamada con 500 registros frente a 500 llamadas: 50 veces más rápido |
| Paginar grandes conjuntos de datos | Medio | Utilice limit y offset: evite cargar registros de 100.000 |
| Caché de datos de sólo lectura | Medio | Caché de catálogos de productos, categorías (TTL 5-15 min) |
Utilice search_count | Bajo | Cuente antes de buscar: evite cargar datos solo para contar |
| Agrupación de conexiones | Medio | Reutilizar sesiones HTTP: ahorra la sobrecarga del protocolo de enlace TLS |
Preguntas frecuentes
¿Cuál es la diferencia entre XML-RPC, JSON-RPC y API REST en Odoo?
XML-RPC es el protocolo heredado disponible en todas las versiones de Odoo; es detallado pero universalmente compatible. JSON-RPC es el protocolo utilizado por el cliente web de Odoo y proporciona la misma funcionalidad con cargas útiles JSON. La API REST se introdujo en Odoo 17 y proporciona puntos finales HTTP estándar con autenticación de clave API, lo que la convierte en la opción más sencilla para las integraciones modernas. Para proyectos nuevos, utilice la API REST si tiene Odoo 17 o posterior.
¿Cómo manejo los límites de velocidad de la API de Odoo?
Odoo.sh aplica límites de tarifas según el nivel de su plan. Cuando reciba una respuesta 429, lea el encabezado Reintentar después y espere antes de volver a intentarlo. Para integraciones de gran volumen, implemente un retroceso exponencial, realice operaciones por lotes para reducir la cantidad de llamadas API y considere usar las acciones programadas de Odoo para el procesamiento masivo en lugar de llamadas API en tiempo real para sincronizaciones no críticas.
¿Puedo llamar a métodos personalizados de Python a través de la API?
Sí. Cualquier método público en un modelo de Odoo se puede llamar a través de XML-RPC o JSON-RPC usando ejecutar_kw. Para la API REST, debe crear un punto final de controlador personalizado con @http.route. Los métodos que comienzan con un guión bajo son privados y no se pueden llamar externamente a través de XML-RPC. Valide siempre las entradas en sus métodos personalizados para evitar ataques de inyección.
¿Cómo sincronizo grandes conjuntos de datos de manera eficiente?
Utilice una combinación de estrategias: sincronización completa inicial con operaciones por lotes y paginación (límite de 200 registros por solicitud), luego sincronizaciones incrementales usando el filtrado write_date para recuperar solo los registros modificados desde la última sincronización. Almacene la marca de tiempo de la última sincronización y utilícela como filtro de dominio. Para conjuntos de datos muy grandes que superan los 100.000 registros, considere la replicación directa de la base de datos en lugar de la sincronización API.
¿Está disponible la API REST de Odoo en Odoo Community Edition?
La API REST nativa con autenticación de clave API se introdujo en Odoo 17 Enterprise. Para Odoo Community, puede usar XML-RPC o JSON-RPC que están disponibles en todas las ediciones, o instalar módulos comunitarios como el marco de descanso de OCA que agrega puntos finales RESTful. Los servicios de integración de ECOSIRE son compatibles con todas las ediciones de Odoo y protocolos API.
¿Cómo manejo los campos Many2many y One2many en llamadas API?
Los campos relacionales utilizan tuplas de comandos especiales: (0, 0, valores) para crear y vincular un nuevo registro, (1, id, valores) para actualizar un registro vinculado, (2, id, 0) para eliminar un registro vinculado, (3, id, 0) para desvincular sin eliminar, (4, id, 0) para vincular un registro existente, (5, 0, 0) para desvincular todo y (6, 0, [ids]) para reemplazar todos los enlaces. Para la lectura, estos campos devuelven listas de ID de forma predeterminada; utilice search_read con el nombre del campo para obtener datos completos.
Próximos pasos
La integración de API es la columna vertebral de los sistemas empresariales modernos. Ya sea que esté creando una sincronización de datos simple o una orquestación multiplataforma compleja, los patrones de esta guía le serán de gran utilidad.
Recursos relacionados:
- Guía de desarrollo de Odoo Python — Profundización en el backend de Python de Odoo
- Guía de depuración de webhook: solución de problemas de integraciones de webhook
- ECOSIRE Marketplace Connectors: integraciones prediseñadas para las principales plataformas
¿Necesita ayuda con la integración de la API de Odoo? El equipo de integración de ECOSIRE ha conectado Odoo a más de 50 plataformas externas, incluidas Shopify, Amazon, Salesforce y ERP personalizados. Desde simples sincronizaciones de datos hasta orquestación bidireccional en tiempo real, creamos integraciones que escalan. Programe una 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.
Artículos relacionados
Segmentación de clientes impulsada por IA: del RFM a la agrupación predictiva
Descubra cómo la IA transforma la segmentación de clientes desde el análisis RFM estático hasta la agrupación predictiva dinámica. Guía de implementación con Python, Odoo y datos reales de ROI.
IA para la optimización de la cadena de suministro: visibilidad, predicción y automatización
Transforme las operaciones de la cadena de suministro con IA: detección de demanda, calificación de riesgos de proveedores, optimización de rutas, automatización de almacenes y predicción de interrupciones. Guía 2026.
Patrones de integración de API: mejores prácticas de arquitectura empresarial
Dominar los patrones de integración de API para sistemas empresariales. REST vs GraphQL vs gRPC, arquitectura basada en eventos, patrón saga, puerta de enlace API y guía de versiones.