Especificación OpenAPI 3 para generar clientes o importar en Postman.

openapi.yaml

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"
}

credential y webhook_secret solo 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étodoRutaUso
POST/link/redeemCanjear código (sin credencial).
GET/storeEstado de la tienda, plan y uso.
PUT/webhookConfigurar URL ({ "url", "rotate_secret" }).
DELETE/webhookQuitar webhook (queda solo sondeo).
POST/webhook/testEnvía un evento ping firmado.
GET/categoriesCategorí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/batchLote de hasta 100 productos.
POST/products/{source}/{external_id}/unpublishDespublicar (no borra).
DELETE/products/{source}/{external_id}Igual que despublicar.
GET/productsListar sincronizados (source, updated_since).
GET/native-productsProductos que la tienda ya tenía en Xalachi.
POST/native-products/{id}/adoptPasar un producto nativo a la integración.
GET/ordersSondeo (created_since, status, not_acknowledged=true).
GET/orders/{id}Detalle con comprobante de pago.
POST/orders/{id}/ackMarcar el pedido como tomado (idempotente).
POST/credentials/rotateRotar credencial.
POST/link/unlinkDesvincular (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} es factico u odoo y 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 en warnings.
  • category_id debe existir y estar activa (422 unknown_category).
  • published: true → publicado; false → borrador.
  • images es 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 en warnings. Enviar images reemplaza 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_id y variant_label permiten agruparlas.
  • Repetir el PUT con el mismo external_id actualiza 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_id son null si la línea es de un producto nativo.
  • payment.proof es null si 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; 429 con Retry-After).
  • Lote de productos: máx. 100 ítems.

9. Errores

{ "error": { "code": "validation_failed", "message": "Datos inválidos", "details": { } }, "request_id": "req_…" }
HTTPcode
400invalid_request
401invalid_credentials, invalid_link_code
403forbidden
404not_found
409conflict, idempotency_conflict, product_limit_reached, link_code_inactive
422validation_failed, unknown_category
429rate_limited
500internal_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.