Docs

Empezar con la API

Para programadores: primera llamada, autenticación, convenciones, paginación, sincronización incremental, equipo, todos los errores, límites y buenas prácticas.

Revisado el 28 de octubre de 2026

Qué se puede hacer

La API de AtiendeFácil es una API REST con JSON. Con una clave, tu sistema trabaja sobre los datos de un negocio: el de la clave. Lo que escribe pasa por las mismas reglas que el panel, así que hace exactamente lo mismo que el botón equivalente (avisarle al cliente por WhatsApp, imprimir la comanda, quedar en la auditoría).

ArtículoQué cubre
Pedidos y pagosListar y ver pedidos, cambiarles el estado, asignar el repartidor, ponerles el costo del envío, aprobar o rechazar comprobantes, registrar cobros de tu caja y reportar movimientos del banco.
CatálogoProductos y servicios: listar, crear, editar, borrar y sincronizar el catálogo entero con tu sistema (precios, stock, disponibilidad).
Clientes, conversaciones y mensajesBuscar y editar clientes, ver qué chats esperan a una persona, leer su historial, tomar un chat o devolvérselo al bot, y escribirle por WhatsApp a quien ya le escribió al negocio.
Citas y viajesLeer la agenda (negocios de citas) y los viajes (negocios de transporte), filtrando por cliente, empleado o conductor.
Tasa de cambioLeer la tasa BCV y la del negocio, y fijar la tasa manual o volver a la del BCV.
WebhooksQue AtiendeFácil avise a tu servidor al instante de cada pedido, pago, cita, mensaje, cambio del catálogo o de la tasa, y de lo que espera a una persona: un chat escalado, un comprobante por revisar, un envío por cotizar, el WhatsApp desconectado o el saldo bajo.

Además, GET /me dice si el bot está en pausa, si el WhatsApp está conectado y cuánto saldo queda, y GET /team lista al equipo (ver Rutas generales). Para mantener una copia al día sin bajarte todo cada vez, está la sincronización incremental.

Primeros pasos

  1. Pídele al dueño una clave de API

    La crea él (o alguien del equipo con permiso para editar Ajustes) y se enseña una sola vez. Empieza por af_live_. Un negocio puede tener hasta 10 claves activas: lo normal es una por sistema conectado.

    Panel web

    AjustesIntegraciones

    App

    MásAjustesIntegraciones
  2. Guárdala en tu servidor

    En una variable de entorno o en el gestor de secretos que uses. Nunca en el código de una página web ni de una app que se instala en teléfonos: cualquiera la vería.

  3. Haz la primera llamada: GET /me

    Devuelve el negocio al que pertenece la clave. Si responde, estás dentro.

curl https://api.atiendefacil.com/api/v1/me \
  -H "Authorization: Bearer af_live_TU_CLAVE_AQUI"

Respuesta:

json
{
  "company": {
    "id": "0f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
    "name": "Pizzería La Esquina",
    "business_mode": "orders",
    "base_currency": "USD"
  },
  "api_key": {
    "id": "9f1c2b3a-4d5e-4f60-8a7b-1c2d3e4f5a6b",
    "name": "Tienda online",
    "access": "full"
  },
  "bot": { "paused": false, "pause_source": null, "pause_reason": null },
  "whatsapp": { "status": "connected", "phone": "584140000000", "channel": "qr" },
  "balance": { "tokens": 184250, "low": false },
  "location": { "lat": 10.4806, "lng": -66.9036, "address": "Av. Bolívar, local 3, Caracas" }
}

business_mode dice qué tipo de negocio es: orders (pedidos), appointments (citas), mototaxi (transporte con conductores) o support (solo soporte). Te sirve para saber qué partes de la API tienen datos: un negocio de citas no tiene pedidos, uno de transporte no tiene catálogo y uno de soporte no tiene pedidos, citas, pagos ni viajes. api_key.id es el id de tu clave; guárdalo, porque los webhooks lo usan para decirte qué cambios hiciste tú (ver tus propios cambios).base_currency es la moneda en la que el negocio pone sus precios, y bot, whatsapp y balance dicen cómo está la cuenta ahora mismo (ver Rutas generales).

URL base y autenticación

Todas las rutas cuelgan de:

url
https://api.atiendefacil.com/api/v1

Cada petición lleva la clave en la cabecera Authorization:

http
Authorization: Bearer af_live_…

También se acepta X-Api-Key: af_live_…. Solo HTTPS. La clave abre únicamente la API: no sirve para entrar al panel, y el acceso del panel no sirve aquí.

  • Cada clave es de acceso completo o de solo lectura; lo elige el dueño al crearla y no se cambia después. Una de solo lectura puede hacer todos los GET y nada más: cualquier otra petición responde 403 read_only_key, también las que no cambian nada pero van por POST (cotizar un viaje, probar un webhook). Cuál tienes lo dice api_key.access (full o read) en GET /me.
  • Una clave es del negocio, no de una persona: sigue funcionando aunque quien la creó deje el equipo. Deja de funcionar al revocarla o si el negocio se desactiva.
  • Todo lo que haces con ella queda en la auditoría del negocio a nombre de la clave («API · Tienda online»), y los pagos que apruebas o registras quedan firmados así.
  • No hay ningún parámetro para cambiar de negocio. Si pides un id de otro negocio, la respuesta es 404, igual que si no existiera.

Convenciones

Cómo es
FormatoJSON en snake_case. Envía Content-Type: application/json en las peticiones con cuerpo.
Idsuuid en texto (5b7c2e1a-3f4d-4c8e-9a6b-1d2e3f4a1b2c). Un id mal formado en la ruta responde 404.
objectCada recurso dice qué es: order, payment, customer, appointment, ride, message, conversation, product, rate, account, team_member, bank_movement, blocked_number, summary, slot, webhook, webhook_delivery.
FechasISO 8601 en UTC: 2026-09-29T14:03:00.000Z. En los filtros puedes mandar cualquier ISO 8601 con zona (2026-09-29T00:00:00-04:00).
ImportesTexto decimal con dos decimales ("12.50") para no perder céntimos por el camino. Conviértelos con un tipo decimal de tu lenguaje, no con coma flotante. Las tasas llevan cuatro ("36.5000"). Al enviar importes, mándalos como número (12.5).
MonedasUSD, EUR o VES (bolívares). Los precios del catálogo van en la moneda base del negocio.
TeléfonosSolo dígitos, con código de país y sin el +: 584140000000.
VacíoUn dato que no hay va como null; el campo siempre está.

Paginación

Las listas (GET /orders, /customers, /payments…) se piden por páginas con page (desde 1) y limit (de 1 a 100; 25 si no lo mandas). La respuesta siempre tiene esta forma:

json
{
  "data": [ { "id": "…", "object": "order", … }, … ],
  "page": 1,
  "limit": 25,
  "has_more": true
}

has_more: true quiere decir que hay otra página. Para traerlo todo, pide páginas hasta que venga en false:

JavaScript
// Recorre todas las páginas de una lista (aquí, los pedidos entregados de septiembre).
async function all(path, params = {}) {
  const out = [];
  for (let page = 1; ; page++) {
    const qs = new URLSearchParams({ ...params, limit: "100", page: String(page) });
    const res = await fetch(`${API}${path}?${qs}`, {
      headers: { Authorization: `Bearer ${KEY}` },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const body = await res.json();
    out.push(...body.data);
    if (!body.has_more) return out;
  }
}

const orders = await all("/orders", {
  status: "completed",
  created_after: "2026-09-01T00:00:00-04:00",
  created_before: "2026-09-30T23:59:59-04:00",
});

Sincronización incremental

Si tu sistema guarda una copia de los datos (para facturar, para reportes), no hace falta bajártelo todo cada vez. Con updated_after la lista trae solo lo que cambió (se creó o se modificó) desde esa fecha, incluida:

ListaCon updated_after
GET /orders, /customers, /appointments, /payments, /rides, /products, /conversationsFiltra por updated_at en esa fecha o después, y el orden cambia: del cambio más viejo al más nuevo (y por id para desempatar). Se combina con el resto de filtros de cada lista.

Así basta con guardar el updated_at del último que procesaste y, en la siguiente vuelta, pedir desde ahí. El último de la vuelta anterior vuelve a salir (el filtro incluye la fecha exacta): guarda por id y reemplaza, no añadas.

JavaScript
// Sincronización incremental: cada vuelta trae solo lo que cambió desde la anterior.
// Con updated_after la lista va del cambio más viejo al más nuevo, así que basta con
// guardar el updated_at del último que procesaste y volver a pedir desde ahí.
async function syncOrders(db) {
  let since = (await db.get("orders_since")) ?? "2026-01-01T00:00:00Z";
  let page = 1;
  for (;;) {
    const qs = new URLSearchParams({ updated_after: since, limit: "100", page: String(page) });
    const res = await fetch(`${API}/orders?${qs}`, {
      headers: { Authorization: `Bearer ${KEY}` },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const body = await res.json();
    for (const order of body.data) {
      await db.upsertOrder(order); // por id: si ya lo tienes, lo reemplaza
    }
    const last = body.data.at(-1)?.updated_at;
    if (last && last !== since) {
      since = last; // se sigue desde el último visto, otra vez en la página 1
      page = 1;
      await db.set("orders_since", since);
    } else {
      page++; // una página entera con la misma hora: se avanza por posición
    }
    if (!body.has_more) return;
  }
}
  • Lo borrado. Un producto borrado deja de salir en GET /products; con include_deleted=true salen también los borrados, con la fecha en deleted_at, que es lo que necesitas para quitarlos de tu copia. Los clientes y las citas borrados desaparecen de la API: para enterarte, usa los webhooks customer.deleted y appointment.deleted.
  • Los carritos no salen. Un pedido que el bot todavía está armando no aparece en GET /orders hasta que el cliente lo cierra; cuando lo hace, su updated_at cambia y entra en la siguiente vuelta.
  • Webhooks y sincronización se complementan. Los webhooks te avisan al momento; una vuelta de updated_after cada cierto tiempo (cada hora, cada noche) recoge lo que se hubiera perdido si tu servidor estuvo caído.

Reintentar sin duplicar (Idempotency-Key)

Si mandas un POST y la respuesta no te llega (se cortó la red, saltó tu tiempo de espera), no sabes si el pedido se creó. Reintentar a ciegas puede dejarlo dos veces. Para evitarlo, manda una cabecera Idempotency-Key con un valor único por operación (un uuid sirve) y repite la misma en cada reintento:

curl
curl -X POST https://api.atiendefacil.com/api/v1/orders \
  -H "Authorization: Bearer af_live_…" \
  -H "Idempotency-Key: 7f3c2b1a-9d4e-4c6f-8a1b-2c3d4e5f6a7b" \
  -H "Content-Type: application/json" \
  -d '{ … }'
  • La primera vez la petición se ejecuta normal. Si llega otra con la misma clave y el mismo cuerpo, no se ejecuta: recibes la misma respuesta que la primera, con la cabecera Idempotent-Replayed: true.
  • Vale en todo lo que escribe (POST, PUT, PATCH, DELETE); en los GET se ignora. Es opcional: sin la cabecera todo funciona como siempre.
  • Solo se recuerdan las respuestas 2xx, durante 24 horas. Si la primera falló (un 4xx o un 5xx), no quedó nada hecho y el reintento con la misma clave se ejecuta de verdad.
  • La clave es de una sola operación: usarla con otra ruta u otro cuerpo responde 422 idempotency_key_reused. Si la primera petición todavía se está ejecutando, la segunda responde 409 idempotency_in_progress.
  • Hasta 200 caracteres, sin espacios ni acentos. Cada negocio tiene sus propias claves.

Úsala sobre todo donde repetir cuesta dinero o molesta a un cliente: crear pedidos, citas y viajes, registrar cobros y movimientos del banco, y enviar mensajes.

Errores

Los errores de la API responden con un código HTTP 4xx y siempre con esta forma. code es estable (programa contra él); message está en español y es para leerlo una persona, puede cambiar.

json
{
  "error": {
    "code": "invalid_transition",
    "message": "No se puede pasar el pedido de completed a preparing."
  }
}
codeHTTPCuándo sale
unauthorized401Falta la clave, no es válida, fue revocada o el negocio está desactivado.
read_only_key403La clave es de solo lectura y la petición no es un GET. Hace falta una clave de acceso completo.
invalid_idempotency_key400La cabecera Idempotency-Key está vacía, es demasiado larga o lleva espacios o acentos.
idempotency_in_progress409Otra petición con esa misma Idempotency-Key se está ejecutando ahora. Espera unos segundos y reintenta.
idempotency_key_reused422Esa Idempotency-Key ya se usó con otra ruta u otro cuerpo. Una clave por operación.
not_found404El recurso no existe, no es de tu negocio o el id no tiene formato de uuid. También sale con una ruta que no existe bajo /api/v1 y con los pedidos que todavía son carrito (salvo el que espera la cotización del envío).
invalid_body400El cuerpo no cumple lo pedido. El mensaje nombra el campo: «products.3.price: …». En PATCH /customers/{id} también sale si mandas un campo que no se puede editar.
invalid_date400Una fecha de filtro (created_after, created_before, updated_after, from, to) no es ISO 8601.
invalid_status400El filtro status tiene un valor que no existe para ese recurso.
invalid_id400Un filtro de id (customer_id, order_id, appointment_id, ride_id, employee_id, driver_id, delivery_user_id) no es un uuid.
delivery_user_invalid400El delivery_user_id no es de alguien del equipo con rol de repartidor y activo (consulta GET /team).
not_quotable409Le pusiste costo de envío a un pedido que no lo espera: un carrito que el cliente todavía arma, uno que no es de delivery o uno que ya no admite cambiarlo (pagado o en marcha).
awaiting_delivery_quote409Quisiste devolverle al bot una conversación que espera el costo del envío: eso se resuelve con POST /orders/{id}/delivery-quote.
invalid_transition409El pedido no puede pasar de su estado actual al que pides (ver transiciones).
no_payment409Pediste confirmar un pedido que espera pago y no tiene ningún pago que aprobar.
already_resolved409El pago que quieres aprobar o rechazar ya está aprobado o rechazado.
needs_amount400El comprobante no trae monto: al aprobarlo envía amount y currency.
order_cancelled409Intentaste registrar un cobro en un pedido cancelado.
no_conversation404Ese cliente nunca le escribió al negocio: no se le puede mandar un mensaje.
outside_service_window409Canal oficial (API de Meta) y han pasado más de 24 horas desde el último mensaje del cliente.
whatsapp_not_connected409El WhatsApp del negocio no está conectado.
meta_no_credit402Canal oficial (API de Meta) y el saldo del negocio no cubre el mensaje (cada uno descuenta 10.000 tokens). No salió: vuelve a mandarlo después de recargar.
duplicate_name409Ya hay otro producto con ese nombre (sin distinguir mayúsculas).
duplicate_external_id409Ya hay otro producto con ese external_id.
duplicate_barcode409Ya hay otro producto con ese código de barras.
catalog_full409El catálogo llegaría a más de 5000 ítems.
missing_manual_rate400Pediste modo manual sin mandar manual_rate y el negocio no tiene ninguna guardada.

Respuestas que no vienen en ese formato

Unas pocas respuestas las da el servidor antes de llegar a la API y traen error como texto, no como objeto. Mira siempre el código HTTP primero.

HTTPCuándoCuerpo
429Te pasaste del límite por minuto (ver abajo).{ "error": "Rate limit exceeded, retry in … seconds" }
400El cuerpo no es JSON válido.{ "error": "<motivo del analizador>" }
413El cuerpo pesa demasiado: 25 MB en la sincronización del catálogo, 2 MB en el resto.{ "error": "Request body is too large", "code": "FST_ERR_CTP_BODY_TOO_LARGE" }
415Un Content-Type que el servidor no acepta. Manda application/json.{ "error": "Unsupported Media Type", … }
500Fallo nuestro. Reintenta más tarde.{ "error": "internal_error" }

Límites

QuéLímite
Casi todas las rutas120 peticiones por minuto, por clave y por ruta.
POST /messages30 por minuto por clave.
PUT /products/sync10 por minuto por clave, cuerpo de hasta 25 MB y 5000 productos.
Tamaño del cuerpo2 MB (salvo la sincronización).
Resultados por página100 como máximo.

Cada ruta lleva su propia cuenta: consultar GET /orders no gasta lo de GET /customers. Todas las respuestas traen cuánto te queda:

CabeceraQué dice
x-ratelimit-limitPeticiones permitidas en la ventana de un minuto.
x-ratelimit-remainingLas que te quedan.
x-ratelimit-resetSegundos hasta que la cuenta vuelve a cero.
retry-afterSolo en el 429: segundos que tienes que esperar.
http
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit: 120
x-ratelimit-remaining: 0
x-ratelimit-reset: 37
retry-after: 37

{ "error": "Rate limit exceeded, retry in 37 seconds" }

Compatibilidad

  • Esta es la versión 1 (/api/v1). Dentro de ella solo se añaden cosas: campos nuevos en los recursos, rutas nuevas, eventos nuevos. Quitar o renombrar un campo exigiría una /api/v2.
  • Por eso tu código tiene que ignorar los campos que no conozca y no romperse si un estado trae un valor nuevo.
  • Los recursos de la API y los que van dentro de los webhooks tienen exactamente la misma forma: aprendes un solo formato.

Buenas prácticas

  • Webhooks en vez de preguntar a cada rato. Consultar la API cada pocos segundos gasta tu límite y llega tarde; un webhook te avisa al instante. Usa la API para la carga inicial y para leer el detalle.
  • La clave, solo en tu servidor. Un navegador o una app de teléfono no deben hablar con la API directamente: que hablen con tu servidor, y tu servidor con AtiendeFácil.
  • Reintenta con espera ante 429 y 5xx, respetando retry-after. Un 4xx distinto de 429 no se arregla reintentando: lee el code.
  • Cuidado al reintentar lo que crea algo. La API no tiene claves de idempotencia. Si un POST /orders/{id}/payments, un POST /payments/collect o un POST /messages se corta sin respuesta, comprueba antes de repetir (GET /payments?order_id=…) o te arriesgas a registrar el cobro o mandar el mensaje dos veces. PUT, PATCH y POST /bank-movements (que descarta referencias repetidas) sí se pueden repetir sin miedo.
  • Una clave por sistema. Si conectas la tienda online y la contabilidad, que cada una tenga la suya: en la auditoría se sabe quién hizo qué y puedes revocar una sin tumbar la otra.
JavaScript
// Reintenta ante 429 y 5xx, respetando retry-after y esperando cada vez más.
async function call(path, init = {}, tries = 5) {
  for (let i = 0; ; i++) {
    const res = await fetch(`${API}${path}`, {
      ...init,
      headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", ...init.headers },
    });
    const retriable = res.status === 429 || res.status >= 500;
    if (!retriable || i === tries - 1) return res;
    const wait = Number(res.headers.get("retry-after")) || 2 ** i; // segundos
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
}

Rutas generales

GET/me

El negocio de la clave, la propia clave y el estado de la cuenta ahora mismo:

CampoQué es
companyid, name, business_mode y base_currency (USD o EUR: la moneda de sus precios).
api_keyid, name y access (full o read) de la clave que usas.
botpaused (si el asistente dejó de responder), pause_source (manual: lo pausó el negocio; risk: el sistema, para proteger el número) y pause_reason. Sin pausa, los dos últimos van en null.
whatsappstatus: connected, connecting, disconnected (una reconexión de rutina, se arregla sola) o logout (se cerró la sesión: hay que volver a vincular el número). phone, el número vinculado, y channel: qr (código QR) o meta (API oficial).
balancetokens (el saldo, número) y low (true si ya se avisó de saldo bajo y no se ha recargado).
locationLa ubicación del negocio, la de su perfil: { lat, lng, address } (address puede ir en null). Es null si el negocio no marcó su punto en el mapa. Sirve para mandársela a un cliente con POST /conversations/{id}/location.

Lo mismo va dentro de los webhooks bot.*, whatsapp.* y account.balance_low, como objeto account (ver Webhooks).

POST/bot/pause

Pausa el asistente, igual que el interruptor del panel y la app: deja de contestarle a todos los clientes; los mensajes se siguen guardando y los atiende una persona. Lo que el negocio tiene programado sigue saliendo. No lleva cuerpo. Responde el estado de la cuenta (object: "account", con bot, whatsapp y balance, como en GET /me). Si ya estaba en pausa no cambia nada y responde igual. Para callar al asistente en un solo chat usa tomar la conversación.

POST/bot/resume

Lo reanuda, sea cual sea el motivo de la pausa. Si el sistema lo había pausado para proteger el número (pause_source: "risk"), reanudarlo es decisión del negocio: mira antes pause_reason. Responde el estado de la cuenta. Salen los webhooks bot.paused y bot.resumed.

GET/summary

Las cifras del Dashboard, las mismas que ve el dueño en el panel y la app: lo de hoy, lo del mes y lo que está esperando a alguien. Para un tablero propio o un cierre del día sin recorrer las listas.

json
{
  "object": "summary",
  "currency": "USD",
  "today": { "sales": "184.50", "orders": 12, "appointments": 0, "appointment_sales": "0.00", "conversations": 31 },
  "month": { "sales": "2310.00", "appointment_sales": "0.00", "messages_received": 1840, "messages_sent": 2105 },
  "pending": { "conversations": 2, "payments": 1, "to_collect": "45.00", "to_collect_orders": 3 },
  "orders": { "completed": 412, "cancelled": 37, "in_progress": 6 },
  "last_7_days": [ { "date": "2026-10-05", "sales": "150.00" }, … ],
  "top_products": [ { "name": "Pizza de pepperoni", "quantity": 96, "sales": "960.00" }, … ],
  "customers": 538,
  "catalog_items": 64
}
  • Los importes van en currency, la moneda base del negocio. sales suma los pedidos confirmados en adelante (no los que esperan pago ni los cancelados); appointment_sales, el precio de las citas confirmadas o atendidas.
  • «Hoy» y «el mes» se cortan con el reloj del servidor (UTC), igual que en el Dashboard: para un rango exacto en tu hora, filtra GET /orders con created_after y created_before.
  • pending.conversations son los chats que esperan a una persona; pending.payments, los pedidos con el comprobante por revisar; to_collect, lo que falta cobrar en total, y en cuántos pedidos.
  • orders y top_products (los 5 más vendidos) son de todo el historial, no del mes.
GET/team

Las personas del equipo del negocio, ordenadas por nombre y sin paginar. Sirve sobre todo para saber a qué repartidor asignar un pedido. Filtros opcionales: role (owner, admin, agent o delivery) y active=true (solo las activas). En un negocio de transporte, los conductores salen con role: "delivery"; su id es el driver.id de los viajes.

curl
curl "https://api.atiendefacil.com/api/v1/team?role=delivery&active=true" \
  -H "Authorization: Bearer af_live_TU_CLAVE_AQUI"
json
{
  "data": [
    { "id": "d9e0f1a2-b3c4-4d5e-8f6a-7b8c9d0e1f2a", "object": "team_member", "name": "Carlos", "role": "delivery", "active": true },
    { "id": "b3c4d5e6-f7a8-4b9c-8d0e-1f2a3b4c5d6e", "object": "team_member", "name": "Luis", "role": "delivery", "active": true }
  ]
}
GET/webhook-events

La lista de eventos que puede recibir un webhook, como { "data": [ { "type": "order.created", "description": "…" }, … ] }. No va paginada. Qué trae cada uno está en Webhooks.

WhatsApp: vincular el número y enviar avisos

Para el sistema que quiere un solo WhatsApp: vincula el número del negocio desde su propia pantalla y manda por él sus avisos (facturas, recordatorios), en vez de mantener otra sesión aparte.

PeticiónQué hace
GET /whatsappEl estado del canal: status, phone, channel (qr o meta) y qr, la imagen del código mientras espera a que lo escaneen.
POST /whatsapp/connectEmpieza a vincular. Después consulta GET /whatsapp cada pocos segundos: el código caduca y cambia, y al escanearlo el estado pasa a connected. Responde 422 profile_incomplete si falta completar el perfil del negocio.
POST /whatsapp/logoutDesvincula el número.
POST /whatsapp/sendManda un aviso a cualquier número. Cuerpo: phone y text, document (base64, file_name, mime_type; hasta 25 MB) o los dos (el texto va como pie del archivo). Opcional: template ({ key }), solo con text, para el canal de Meta fuera de la ventana de 24 horas (abajo).
  • POST /whatsapp/send no es el chat: no abre conversación ni pasa por el asistente. Para responderle a un cliente que escribió usa POST /messages.
  • Cada aviso pasa por los mismos topes y pausas que protegen al número del bloqueo. Si no sale, responde 409 not_sent (el número no tiene WhatsApp o se llegó al tope) o 409 whatsapp_not_connected (no hay número vinculado). Con el canal de Meta, si el saldo no cubre el mensaje (10.000 tokens cada uno), responde 402 meta_no_credit y no sale.
  • Con el canal de Meta no hay código que escanear. Dentro de la ventana de 24 horas de ese número (desde su último mensaje al negocio) sale lo que mandes, como por QR.
  • Cambio desde el 15/10/2026: con el canal de Meta, fuera de la ventana de 24 horas, un texto ya no se mete solo en la plantilla de avisos al cliente. Responde 409 outside_service_window («fuera de esa ventana solo salen plantillas aprobadas»), salvo que pidas una plantilla con template.key: customer_notice (cualquier aviso), order_status (estado de un pedido), payment_result (resultado de un pago), appointment_update (novedad de una cita) o ride_update (novedad de un viaje). Tu text va dentro, en una sola línea, con el nombre del negocio delante (los textos de cada una, en Plantillas de Meta). Úsalas solo para lo que dice su nombre: una promoción dentro de una plantilla de servicio hace que Meta la reclasifique o la pause para todo el negocio. Otra clave responde 400 invalid_template, y una que Meta aún no aprobó en el número, 409 template_not_approved. Por QR, o dentro de la ventana, template se ignora.
  • Un documento fuera de la ventana con el canal de Meta responde 409 outside_service_window antes de intentarlo (antes salía not_sent). Cada mensaje que sale por Meta descuenta su tarifa del saldo del negocio, como cualquier otro.

Preguntas frecuentes

¿Usar la API gasta saldo del negocio?

No. Las consultas, los cambios y los webhooks no gastan tokens. Lo que sí puede gastar es lo que la IA haga después (por ejemplo, contestar al cliente que responde a tu mensaje), igual que si el mensaje lo hubieras escrito desde el panel.

¿Puedo crear pedidos o citas por la API?

Sí: POST /orders, POST /appointments y POST /rides crean lo que no entró por WhatsApp, igual que los botones «Nuevo pedido», «Nueva cita» y «Nuevo viaje» del panel y la app. Están en Pedidos y pagos y en Citas y viajes.

¿Hay un entorno de pruebas?

No hay uno aparte. Lo más seguro es probar con una clave del propio negocio y empezar por las rutas de lectura. Para los webhooks, el botón Probar del panel manda un aviso de prueba (ver Probar en tu computadora).

¿Puedo llamar a la API desde el navegador?

No lo hagas: tendrías que poner la clave en la página, y quien la abra se la lleva. Llama desde tu servidor.

¿No encontraste lo que buscabas?

Escríbenos y te responde una persona del equipo.