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ículo | Qué cubre |
|---|---|
| Pedidos y pagos | Listar 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álogo | Productos y servicios: listar, crear, editar, borrar y sincronizar el catálogo entero con tu sistema (precios, stock, disponibilidad). |
| Clientes, conversaciones y mensajes | Buscar 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 viajes | Leer la agenda (negocios de citas) y los viajes (negocios de transporte), filtrando por cliente, empleado o conductor. |
| Tasa de cambio | Leer la tasa BCV y la del negocio, y fijar la tasa manual o volver a la del BCV. |
| Webhooks | Que 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
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
AjustesIntegracionesApp
MásAjustesIntegracionesGuá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.
Haz la primera llamada:
GET /meDevuelve 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"// Node.js 18+ (fetch viene incluido). Nunca en el navegador: la clave quedaría a la vista.
const API = "https://api.atiendefacil.com/api/v1";
const KEY = process.env.ATIENDEFACIL_API_KEY; // af_live_…
const res = await fetch(`${API}/me`, {
headers: { Authorization: `Bearer ${KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`);
console.log(body.company.name, body.company.business_mode);<?php
$api = 'https://api.atiendefacil.com/api/v1';
$key = getenv('ATIENDEFACIL_API_KEY'); // af_live_…
$ch = curl_init("$api/me");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new Exception("$status {$body['error']['code']}: {$body['error']['message']}");
}
echo $body['company']['name'];import os
import requests # pip install requests
API = "https://api.atiendefacil.com/api/v1"
KEY = os.environ["ATIENDEFACIL_API_KEY"] # af_live_…
res = requests.get(f"{API}/me", headers={"Authorization": f"Bearer {KEY}"}, timeout=15)
body = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {body['error']['code']}: {body['error']['message']}")
print(body["company"]["name"], body["company"]["business_mode"])Respuesta:
{
"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:
https://api.atiendefacil.com/api/v1Cada petición lleva la clave en la cabecera Authorization:
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
GETy nada más: cualquier otra petición responde403 read_only_key, también las que no cambian nada pero van porPOST(cotizar un viaje, probar un webhook). Cuál tienes lo diceapi_key.access(fulloread) enGET /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 | |
|---|---|
| Formato | JSON en snake_case. Envía Content-Type: application/json en las peticiones con cuerpo. |
| Ids | uuid en texto (5b7c2e1a-3f4d-4c8e-9a6b-1d2e3f4a1b2c). Un id mal formado en la ruta responde 404. |
| object | Cada 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. |
| Fechas | ISO 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). |
| Importes | Texto 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). |
| Monedas | USD, EUR o VES (bolívares). Los precios del catálogo van en la moneda base del negocio. |
| Teléfonos | Solo dígitos, con código de país y sin el +: 584140000000. |
| Vacío | Un 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:
{
"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:
// 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:
| Lista | Con updated_after |
|---|---|
GET /orders, /customers, /appointments, /payments, /rides, /products, /conversations | Filtra 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.
// 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; coninclude_deleted=truesalen también los borrados, con la fecha endeleted_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 webhookscustomer.deletedyappointment.deleted. - Los carritos no salen. Un pedido que el bot todavía está armando no aparece en
GET /ordershasta que el cliente lo cierra; cuando lo hace, suupdated_atcambia y entra en la siguiente vuelta. - Webhooks y sincronización se complementan. Los webhooks te avisan al momento; una vuelta de
updated_aftercada 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 -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 losGETse ignora. Es opcional: sin la cabecera todo funciona como siempre. - Solo se recuerdan las respuestas
2xx, durante 24 horas. Si la primera falló (un4xxo un5xx), 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 responde409 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.
{
"error": {
"code": "invalid_transition",
"message": "No se puede pasar el pedido de completed a preparing."
}
}| code | HTTP | Cuándo sale |
|---|---|---|
unauthorized | 401 | Falta la clave, no es válida, fue revocada o el negocio está desactivado. |
read_only_key | 403 | La clave es de solo lectura y la petición no es un GET. Hace falta una clave de acceso completo. |
invalid_idempotency_key | 400 | La cabecera Idempotency-Key está vacía, es demasiado larga o lleva espacios o acentos. |
idempotency_in_progress | 409 | Otra petición con esa misma Idempotency-Key se está ejecutando ahora. Espera unos segundos y reintenta. |
idempotency_key_reused | 422 | Esa Idempotency-Key ya se usó con otra ruta u otro cuerpo. Una clave por operación. |
not_found | 404 | El 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_body | 400 | El 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_date | 400 | Una fecha de filtro (created_after, created_before, updated_after, from, to) no es ISO 8601. |
invalid_status | 400 | El filtro status tiene un valor que no existe para ese recurso. |
invalid_id | 400 | Un filtro de id (customer_id, order_id, appointment_id, ride_id, employee_id, driver_id, delivery_user_id) no es un uuid. |
delivery_user_invalid | 400 | El delivery_user_id no es de alguien del equipo con rol de repartidor y activo (consulta GET /team). |
not_quotable | 409 | Le 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_quote | 409 | Quisiste devolverle al bot una conversación que espera el costo del envío: eso se resuelve con POST /orders/{id}/delivery-quote. |
invalid_transition | 409 | El pedido no puede pasar de su estado actual al que pides (ver transiciones). |
no_payment | 409 | Pediste confirmar un pedido que espera pago y no tiene ningún pago que aprobar. |
already_resolved | 409 | El pago que quieres aprobar o rechazar ya está aprobado o rechazado. |
needs_amount | 400 | El comprobante no trae monto: al aprobarlo envía amount y currency. |
order_cancelled | 409 | Intentaste registrar un cobro en un pedido cancelado. |
no_conversation | 404 | Ese cliente nunca le escribió al negocio: no se le puede mandar un mensaje. |
outside_service_window | 409 | Canal oficial (API de Meta) y han pasado más de 24 horas desde el último mensaje del cliente. |
whatsapp_not_connected | 409 | El WhatsApp del negocio no está conectado. |
meta_no_credit | 402 | Canal 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_name | 409 | Ya hay otro producto con ese nombre (sin distinguir mayúsculas). |
duplicate_external_id | 409 | Ya hay otro producto con ese external_id. |
duplicate_barcode | 409 | Ya hay otro producto con ese código de barras. |
catalog_full | 409 | El catálogo llegaría a más de 5000 ítems. |
missing_manual_rate | 400 | Pediste 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.
| HTTP | Cuándo | Cuerpo |
|---|---|---|
| 429 | Te pasaste del límite por minuto (ver abajo). | { "error": "Rate limit exceeded, retry in … seconds" } |
| 400 | El cuerpo no es JSON válido. | { "error": "<motivo del analizador>" } |
| 413 | El 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" } |
| 415 | Un Content-Type que el servidor no acepta. Manda application/json. | { "error": "Unsupported Media Type", … } |
| 500 | Fallo nuestro. Reintenta más tarde. | { "error": "internal_error" } |
Límites
| Qué | Límite |
|---|---|
| Casi todas las rutas | 120 peticiones por minuto, por clave y por ruta. |
POST /messages | 30 por minuto por clave. |
PUT /products/sync | 10 por minuto por clave, cuerpo de hasta 25 MB y 5000 productos. |
| Tamaño del cuerpo | 2 MB (salvo la sincronización). |
| Resultados por página | 100 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:
| Cabecera | Qué dice |
|---|---|
x-ratelimit-limit | Peticiones permitidas en la ventana de un minuto. |
x-ratelimit-remaining | Las que te quedan. |
x-ratelimit-reset | Segundos hasta que la cuenta vuelve a cero. |
retry-after | Solo en el 429: segundos que tienes que esperar. |
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. Un4xxdistinto de 429 no se arregla reintentando: lee elcode. - Cuidado al reintentar lo que crea algo. La API no tiene claves de idempotencia. Si un
POST /orders/{id}/payments, unPOST /payments/collecto unPOST /messagesse 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,PATCHyPOST /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.
// 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
El negocio de la clave, la propia clave y el estado de la cuenta ahora mismo:
| Campo | Qué es |
|---|---|
company | id, name, business_mode y base_currency (USD o EUR: la moneda de sus precios). |
api_key | id, name y access (full o read) de la clave que usas. |
bot | paused (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. |
whatsapp | status: 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). |
balance | tokens (el saldo, número) y low (true si ya se avisó de saldo bajo y no se ha recargado). |
location | La 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).
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.
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.
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.
{
"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.salessuma 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 /ordersconcreated_afterycreated_before. pending.conversationsson 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.ordersytop_products(los 5 más vendidos) son de todo el historial, no del mes.
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 "https://api.atiendefacil.com/api/v1/team?role=delivery&active=true" \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI"{
"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 }
]
}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ón | Qué hace |
|---|---|
GET /whatsapp | El estado del canal: status, phone, channel (qr o meta) y qr, la imagen del código mientras espera a que lo escaneen. |
POST /whatsapp/connect | Empieza 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/logout | Desvincula el número. |
POST /whatsapp/send | Manda 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/sendno es el chat: no abre conversación ni pasa por el asistente. Para responderle a un cliente que escribió usaPOST /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) o409 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), responde402 meta_no_credity 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 contemplate.key:customer_notice(cualquier aviso),order_status(estado de un pedido),payment_result(resultado de un pago),appointment_update(novedad de una cita) oride_update(novedad de un viaje). Tutextva 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 responde400 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,templatese ignora. - Un documento fuera de la ventana con el canal de Meta responde
409 outside_service_windowantes de intentarlo (antes salíanot_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.
