Docs

API: recetas paso a paso

Casos completos con la API y los webhooks: entregar una póliza, una entrada o una factura cuando el cliente paga, descontar inventario, atender desde tu herramienta y avisar al cliente desde tu sistema.

Revisado el 28 de octubre de 2026

Cómo se arma una integración

Casi todas siguen el mismo dibujo: AtiendeFácil te avisa de que pasó algo (un webhook), tu sistema hace su parte y, si hace falta, le contesta al cliente por el mismo chat con la API. Lo único que tienes que montar es un programa pequeño que reciba los avisos; el bot, el cobro y el chat siguen siendo los de AtiendeFácil.

Quieres…Te avisaTú llamas a
Entregar algo cuando el cliente paga (una póliza, una entrada, una licencia, una factura)order.status_changed con confirmedPOST /conversations/{id}/media
Descontar de tu inventario lo que vende el botorder.createdTu propio sistema
Que el bot venda con tus precios y existencias—PUT /products/sync
Atender desde tu herramienta a quien pide una personaconversation.needs_agent/takeover, POST /messages y /resume
Avisar al cliente de algo que pasó en tu sistema—POST /messages
Que el bot reconozca a tus clientes por su nombrecustomer.lookupRespondes al propio aviso
Confirmar pagos con lo que ve tu banco—POST /bank-movements

Entregar algo cuando el cliente paga

El caso típico: el negocio vende algo que genera otro sistema. Una aseguradora vende pólizas por WhatsApp; el bot atiende, arma el pedido y cobra, y cuando el pago queda validado, el sistema de la aseguradora emite la póliza y el cliente la recibe en el mismo chat. Vale igual para entradas, licencias, cupones, facturas o un código de retiro.

  1. Carga lo que vendes como productos

    Cada plan es un producto del catálogo, con su precio. Ponle en external_id el código con el que tu sistema lo conoce: vuelve en cada línea del pedido. Ver API: catálogo y sincronización.
  2. Suscríbete a order.status_changed

    En Ajustes → Integraciones crea un webhook con ese evento y una clave de API que pueda escribir. Ver Integraciones: API y webhooks.
  3. Espera a que el pedido quede confirmed

    Un pedido pasa a confirmed cuando sus pagos aprobados cubren lo que se le pedía. El aviso trae el pedido entero: el chat (conversation_id), el cliente, las líneas y la nota.
  4. Genera el archivo y mándalo al chat

    Con POST /conversations/{id}/media, como document: en base64 o, si ya lo tienes publicado, con su url. Ver enviar fotos, archivos y audio.

El aviso que llega

json
{
  "id": "0c6f1e2d-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
  "type": "order.status_changed",
  "created_at": "2026-09-29T18:03:40.130Z",
  "company_id": "0f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
  "data": {
    "object": {
      "id": "5b7c2e1a-3f4d-4c8e-9a6b-1d2e3f4a1b2c",
      "object": "order",
      "number": "4A1B2C",
      "status": "confirmed",
      "is_credit": false,
      "fully_paid": true,
      "balance_due": "0.00",
      "total": "120.00",
      "currency": "USD",
      "note": "Toyota Corolla 2018, placa AB123CD",
      "customer": { "id": "c4d5e6f7-…", "name": "María Pérez", "phone": "584140000000" },
      "conversation_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
      "items": [
        { "name": "Seguro RCV anual", "quantity": 1, "unit_price": "120.00", "notes": null,
          "product_id": "a1b2c3d4-…", "external_id": "RCV-ANUAL", "barcode": null }
      ],
      "payments": [ { "id": "7a8b9c0d-…", "status": "approved", "amount": "120.00", … } ],
      …
    },
    "status": "confirmed",
    "previous_status": "pending_review"
  }
}

El manejador completo

js
// Sigue al receptor de «Webhooks»: aquí llega cada aviso ya verificado y sin repetir.
const API = "https://api.atiendefacil.com/api/v1";
const KEY = process.env.ATIENDEFACIL_API_KEY; // af_live_…

async function handle(event) {
  if (event.type !== "order.status_changed") return;
  const order = event.data.object;

  // "confirmed" = el pedido quedó cubierto. Uno a crédito (fiado) también llega a
  // "confirmed", pero sin haber pagado: fully_paid lo distingue.
  if (event.data.status !== "confirmed" || !order.fully_paid) return;
  if (!order.conversation_id) return; // creado sin chat: no hay dónde mandarlo

  // 1. Tu sistema genera lo que se vendió (la póliza, la entrada, la licencia…).
  //    external_id es el código con el que TÚ conoces el producto.
  const pdf = await miSistema.emitir({
    referencia: order.id, // úsala para no emitir dos veces lo mismo
    planes: order.items.map((i) => i.external_id ?? i.name),
    datos: order.note, // lo que el cliente dictó: vehículo, placa…
    cliente: order.customer,
  }); // → Buffer con el PDF

  // 2. Lo mandas al mismo chat donde el cliente compró.
  const res = await fetch(`${API}/conversations/${order.conversation_id}/media`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
      // Si reintentas, el cliente no recibe el archivo dos veces.
      "Idempotency-Key": `entrega-${order.id}`,
    },
    body: JSON.stringify({
      kind: "document",
      mime_type: "application/pdf",
      file_name: `poliza-${order.number}.pdf`,
      caption: "¡Listo! Aquí está tu póliza. Guárdala en tu teléfono.",
      base64: pdf.toString("base64"),
    }),
  });

  if (res.status === 409) {
    // outside_service_window o whatsapp_not_connected: no salió. Guárdalo y avisa a alguien.
    const { error } = await res.json();
    await miSistema.pendienteDeEntrega(order.id, error.code);
    return;
  }
  if (!res.ok) throw new Error(`No se pudo entregar el pedido ${order.number}: ${res.status}`);
}

Lo que hay que tener en cuenta

  • Por qué el pedido y no el pago. Un pedido puede pagarse en varias partes o con varios métodos, y cada parte dispara su propio payment.status_changed. El pedido solo pasa a confirmed una vez, cuando la suma alcanza.
  • A crédito. Un pedido fiado llega a confirmed sin haber pagado: viene con is_credit: true y fully_paid: false. El ejemplo no le entrega; si a esos también les entregas, quita esa condición.
  • Cuánto falta. El pedido trae fully_paid, amount_paid y balance_due en su propia moneda: no hace falta sumar pagos ni convertir bolívares.
  • Los datos que no son un producto. Lo que el cliente dicta (la placa, el modelo, a nombre de quién) queda en note y en el notes de cada línea, como texto libre. No hay campos propios por negocio: si necesitas un dato con formato, valídalo en tu sistema y, si falta, pídeselo al cliente con POST /messages.
  • Precios fijos. El bot cobra el precio del catálogo. Si tu precio depende de lo que diga el cliente (el año del carro, la edad), el bot no puede cotizarlo consultando tu sistema: carga un producto por tramo o cotiza tú y crea el pedido con POST /orders.
  • Ventana de 24 horas. Con el canal oficial de WhatsApp solo se puede escribir dentro de las 24 horas siguientes al último mensaje del cliente; pasado eso responde 409 outside_service_window. Si emites al momento no te afecta, pero guarda lo que no salió.
  • Que no salga dos veces. Un aviso puede repetirse. Guarda el id del evento, emite con el id del pedido como referencia y manda la cabecera Idempotency-Key.

Descontar de tu inventario lo que vende el bot

Cada pedido que cierra el bot dispara order.created con sus líneas. Si tus productos llevan external_id (o código de barras), cada línea te dice cuál fue y cuánto. En sentido contrario, PUT /products/sync le pasa al bot tus precios y existencias: ver API: catálogo y sincronización.

js
async function handle(event) {
  if (event.type !== "order.created") return;
  const order = event.data.object;

  for (const item of order.items) {
    // Las líneas que no casaron con el catálogo llegan sin external_id: revísalas a mano.
    if (!item.external_id) continue;
    await miInventario.descontar(item.external_id, item.quantity, { pedido: order.id });
  }
}

quantity es un número con hasta tres decimales (hay productos que se venden por peso). Si un pedido se cancela llega order.status_changed con cancelled: devuelve entonces lo descontado.

Atender desde tu herramienta a quien pide una persona

Cuando el bot no puede resolver algo, marca el chat y dispara conversation.needs_agent. Tu sistema puede avisar al equipo, tomar el chat para que el bot no conteste encima, escribir con POST /messages y devolvérselo al bot al terminar. Ver conversaciones.

js
async function handle(event) {
  if (event.type !== "conversation.needs_agent") return;
  const chat = event.data.object;

  await miEquipo.avisar(`${chat.customer.name ?? chat.customer.phone} pide hablar con una persona`);

  // Cuando alguien de tu equipo lo abra en tu herramienta: el bot se calla en ese chat…
  await fetch(`${API}/conversations/${chat.id}/takeover`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}` },
  });
  // …y al terminar, POST /conversations/{id}/resume se lo devuelve al bot.
}

Avisar al cliente de algo que pasó en tu sistema

Un envío que salió, un resultado listo, un cambio de horario: tu sistema escribe por el WhatsApp del negocio con POST /messages, con el teléfono o con el customer_id. No hace falta conocer el chat.

curl
curl -X POST https://api.atiendefacil.com/api/v1/messages   -H "Authorization: Bearer af_live_TU_CLAVE_AQUI"   -H "Content-Type: application/json"   -H "Idempotency-Key: guia-MRW-88213"   -d '{ "phone": "584140000000", "text": "Tu paquete ya salió. Guía MRW 88213." }'

Vale la misma ventana de 24 horas del canal oficial, y son mensajes de servicio a quien ya es cliente: para avisos a mucha gente a la vez usa los estados o, en tus grupos, los mensajes programados.

Preguntas frecuentes

¿Puedo esperar al pago en vez de al pedido?

Sí. payment.status_changed con approved trae order_id y conversation_id, así que puedes contestar en el chat sin pedir el pedido. Úsalo para acusar recibo de cada abono; para entregar, espera al pedido.

¿Y si mi sistema tarda en generar el archivo?

Responde 200 al aviso enseguida y haz el trabajo después: un aviso que tarda más de 10 segundos en contestarse se da por fallido y se reintenta. Si tu emisión tarda minutos, manda antes un texto («Estamos generando tu póliza») para que el cliente no se quede esperando sin saber.

¿El bot puede consultar mi sistema en mitad de la conversación?

Solo para reconocer al cliente: customer.lookup le pregunta a tu sistema quién es un número nuevo. Ver Webhooks. Para precios o disponibilidad, el bot lee el catálogo: mantenlo al día con PUT /products/sync.

¿Qué pasa si el archivo no salió?

La API te lo dice en la respuesta (409 whatsapp_not_connected, 409 outside_service_window, 413 file_too_large). No hay reintento automático de lo que mandas tú: guarda lo pendiente y vuelve a intentarlo con la misma Idempotency-Key.

¿No encontraste lo que buscabas?

Escríbenos y te responde una persona del equipo.