API de integraciones de Xalachi
API para que Fáctico PRO y Odoo (u otro sistema) publiquen el catálogo de una tienda en Xalachi y reciban sus pedidos.
- Base:
https://xalachi.com/api/integrations/v1 - Formato: JSON UTF-8. Fechas ISO 8601 en UTC. Montos en colones (CRC) enteros.
- Especificación OpenAPI 3:
openapi.yaml - Todas las respuestas exitosas incluyen
store_status(activa|suspendida|pendiente) para que el sistema de origen avise si la tienda no está activa.
1. Cómo se obtiene el código de vinculación
El superadministrador de Xalachi abre Superadmin › Tiendas › Integración y pulsa Generar código. El código tiene el formato XLC-XXXX-XXXX-XXXX-XXXX y:
- no vence: sirve hasta que se desactive o se regenere;
- se puede canjear de nuevo (por ejemplo al reinstalar Odoo); cada canje revoca la credencial anterior y entrega una nueva;
- no contiene datos de la tienda.
2. Canje
curl -X POST https://xalachi.com/api/integrations/v1/link/redeem \
-H "Content-Type: application/json" \
-d '{
"code": "XLC-7K2M-Q9TR-4HWP-8NXA",
"source": "odoo",
"external_account_label": "INVERSIONES RAHI S.A. (Odoo 18)",
"webhook_url": "https://erp.cliente.com/kibu_xalachi/webhook"
}'
webhook_url es opcional (si el sistema no tiene URL pública, usa sondeo de pedidos). Respuesta 201:
{
"store": { "id": "uuid", "name": "Superpan", "slug": "superpan", "url": "https://xalachi.com/superpan", "status": "activa", "plan": "Profesional", "is_courtesy": true, "max_products": 100 },
"credential": "xlc_live_…",
"webhook_secret": "whsec_…",
"store_status": "activa"
}
credentialywebhook_secretsolo se muestran aquí (y al rotar). Guardalos cifrados en el sistema de origen. Xalachi guarda solo el hash de la credencial.
Errores: 401 invalid_link_code, 409 link_code_inactive, 429 rate_limited (10 intentos/hora por IP; tras 5 fallidos, bloqueo de 15 min).
3. Autenticación
Todas las demás llamadas llevan:
Authorization: Bearer xlc_live_…
La credencial pertenece a una sola tienda; nunca puede leer ni escribir otra. Si el vínculo se desvincula o la credencial se revoca → 401 invalid_credentials.
Rotación: POST /credentials/rotate devuelve una credencial nueva; la actual sigue sirviendo 10 minutos.
4. Endpoints
| Método | Ruta | Uso |
|---|---|---|
| POST | /link/redeem | Canjear código (sin credencial). |
| GET | /store | Estado de la tienda, plan y uso. |
| PUT | /webhook | Configurar URL ({ "url", "rotate_secret" }). |
| DELETE | /webhook | Quitar webhook (queda solo sondeo). |
| POST | /webhook/test | Envía un evento ping firmado. |
| GET | /categories | Categorías del marketplace para mapear. |
| PUT | /products/{source}/{external_id} | Crear o actualizar producto. |
| GET | /products/{source}/{external_id} | Leer un producto sincronizado. |
| POST | /products/batch | Lote de hasta 100 productos. |
| POST | /products/{source}/{external_id}/unpublish | Despublicar (no borra). |
| DELETE | /products/{source}/{external_id} | Igual que despublicar. |
| GET | /products | Listar sincronizados (source, updated_since). |
| GET | /native-products | Productos que la tienda ya tenía en Xalachi. |
| POST | /native-products/{id}/adopt | Pasar un producto nativo a la integración. |
| GET | /orders | Sondeo (created_since, status, not_acknowledged=true). |
| GET | /orders/{id} | Detalle con comprobante de pago. |
| POST | /orders/{id}/ack | Marcar el pedido como tomado (idempotente). |
| POST | /credentials/rotate | Rotar credencial. |
| POST | /link/unlink | Desvincular (no borra productos ni pedidos). |
Productos
curl -X PUT https://xalachi.com/api/integrations/v1/products/odoo/prod-9912 \
-H "Authorization: Bearer $XALACHI_CREDENTIAL" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5d0b5b3e-1c1a-4c8e-9b0a-0d4f1f8d2a11" \
-d '{
"name": "Camisa — Rojo, M",
"description": "Camisa de algodón",
"price_crc": 12500,
"stock": 8,
"sku": "CAM-R-M",
"category_id": "uuid-de-categories",
"published": true,
"external_parent_id": "tmpl-551",
"variant_label": "Rojo, M",
"images": [{ "url": "https://origen.example/img/551.jpg", "alt": "Camisa roja", "is_primary": true }]
}'
Reglas:
{source}esfacticouodooy debe coincidir con el origen del vínculo.- Precio: número ≥ 0 en colones. Si trae decimales, Xalachi lo redondea al colón más cercano (
12500.45 → 12500,12500.50 → 12501) y lo indica enwarnings. category_iddebe existir y estar activa (422 unknown_category).published: true→ publicado;false→ borrador.imageses opcional: sin imágenes se publica igual. Xalachi descarga y copia cada imagen (solo https, máx. 5 MB, jpeg/png/webp/gif). Si una falla, el producto se guarda y se avisa enwarnings. Enviarimagesreemplaza el juego de imágenes; omitirlo las deja como están.- Variantes: Xalachi no tiene variantes; cada variante se publica como un producto con su propio
external_id.external_parent_idyvariant_labelpermiten agruparlas. - Repetir el
PUTcon el mismoexternal_idactualiza el mismo producto (no duplica). - Tienda suspendida: se acepta igual y se responde
store_status: "suspendida". - Si se supera el tope de productos del plan →
409 product_limit_reached.
Respuesta 201 (creado) o 200 (actualizado):
{ "product": { "id": "uuid", "source": "odoo", "external_id": "prod-9912", "status": "publicado", "slug": "camisa-rojo-m", "url": "https://xalachi.com/superpan/camisa-rojo-m", "updated_at": "…" }, "warnings": [], "store_status": "activa" }
En el panel de Xalachi, los productos de integración aparecen como «Sincronizado desde Fáctico PRO / Odoo», en solo lectura y sin botón de borrar (la regla también está en la base de datos). Tras desvincular siguen bloqueados hasta que el superadmin los libere.
Productos que la tienda ya tenía
curl "https://xalachi.com/api/integrations/v1/native-products?limit=50" -H "Authorization: Bearer $XALACHI_CREDENTIAL"
curl -X POST https://xalachi.com/api/integrations/v1/native-products/<id>/adopt \
-H "Authorization: Bearer $XALACHI_CREDENTIAL" -H "Content-Type: application/json" \
-d '{ "external_id": "art-3345" }'
adopt conserva el mismo id, imágenes y pedidos históricos. Los productos que no se adopten quedan como nativos y no se tocan.
Pedidos
curl "https://xalachi.com/api/integrations/v1/orders?not_acknowledged=true" -H "Authorization: Bearer $XALACHI_CREDENTIAL"
curl -X POST https://xalachi.com/api/integrations/v1/orders/<id>/ack -H "Authorization: Bearer $XALACHI_CREDENTIAL"
{
"order": {
"id": "uuid", "order_number": "XAL-001005", "status": "pendiente_confirmar",
"created_at": "2026-11-20T16:02:11Z", "paid_at": null, "acknowledged_at": null,
"buyer": { "name": "Ana Pérez", "email": "ana@example.com", "phone": "8888-8888", "address": { } },
"items": [{ "product_id": "uuid", "source": "odoo", "external_id": "prod-9912", "name": "Camisa — Rojo, M", "unit_price_crc": 12500, "qty": 2, "subtotal_crc": 25000 }],
"subtotal_crc": 25000,
"shipping": { "method": "Correos de Costa Rica", "price_crc": 2500 },
"total_crc": 27500,
"payment": { "method_type": "sinpe", "method_label": "SINPE Móvil", "proof": { "url": "https://…firmada…", "expires_at": "…", "content_type": "image/jpeg" } },
"notes": "Entregar en la tarde"
},
"store_status": "activa"
}
items[].source/external_idsonnullsi la línea es de un producto nativo.payment.proofesnullsi no hay comprobante. La URL es firmada y vale 1 hora; pedí de nuevo el detalle para obtener una nueva.
5. Webhook de pedidos
Eventos: order.created, order.payment_proof_added, ping.
POST {webhook_url}
Content-Type: application/json
X-Xalachi-Event: order.created
X-Xalachi-Event-Id: 6f0c…uuid
X-Xalachi-Timestamp: 1795000000
X-Xalachi-Signature: v1=<hex(HMAC_SHA256(webhook_secret, timestamp + "." + cuerpo_crudo))>
{ "event_id": "uuid", "type": "order.created", "created_at": "…", "store_id": "uuid", "data": { "order": { "…": "igual que GET /orders/{id}" } } }
Verificación: recalculá la firma sobre el cuerpo crudo, comparala en tiempo constante y rechazá si el timestamp tiene más de 5 minutos.
Python:
import hmac, hashlib, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
ts = headers["X-Xalachi-Timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
expected = "v1=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, headers["X-Xalachi-Signature"])
JavaScript (Node):
import crypto from "node:crypto";
export function verify(secret, headers, rawBody) {
const ts = headers["x-xalachi-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = "v1=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const got = headers["x-xalachi-signature"] ?? "";
return got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}
Reintentos: si no hay respuesta 2xx en 10 s se reintenta con espera de 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h y 24 h (8 intentos). Los reintentos se procesan como mínimo una vez por hora y también cada vez que el sistema llama a la API. Luego queda fallido, visible para el superadmin; el pedido sigue disponible por sondeo.
Idempotencia: el mismo event_id se repite en cada reintento; deduplicá por event_id y por order.id. Xalachi nunca crea dos order.created para la misma orden. El webhook no sigue redirecciones: usá la URL final.
6. Idempotencia de escrituras
Todas las escrituras aceptan Idempotency-Key: <uuid>. Misma clave + mismo cuerpo dentro de 24 h → misma respuesta (cabecera Idempotent-Replayed: true). Misma clave con otro cuerpo → 409 idempotency_conflict.
7. Paginación
Por cursor: ?limit=50&cursor=<opaco> (máx. 200; pedidos máx. 100). Respuesta: { "data": [...], "next_cursor": "..." | null }.
8. Límites
- 120 solicitudes por minuto por credencial (
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset;429conRetry-After). - Lote de productos: máx. 100 ítems.
9. Errores
{ "error": { "code": "validation_failed", "message": "Datos inválidos", "details": { } }, "request_id": "req_…" }
| HTTP | code |
|---|---|
| 400 | invalid_request |
| 401 | invalid_credentials, invalid_link_code |
| 403 | forbidden |
| 404 | not_found |
| 409 | conflict, idempotency_conflict, product_limit_reached, link_code_inactive |
| 422 | validation_failed, unknown_category |
| 429 | rate_limited |
| 500 | internal_error |
10. Desvincular
POST /link/unlink (o el superadmin desde la ficha de la tienda) deja de sincronizar, revoca credenciales y detiene los avisos. No borra ni despublica productos ni pedidos.
