Empezar con la API de AMAI Voice
De cero a tu primera llamada con éxito. Todo lo que hay debajo describe lo que el código hace hoy, no lo que promete un folleto: las tablas de esta página se generan del propio contrato y de las reglas de autorización que ejecuta el servidor.
- Base URL:
https://voice.amai.run— o el dominio de tu proveedor, si contrataste con marca blanca. El contrato es idéntico. - Autenticación:
Authorization: Bearer amai_<key> - Superficie: 134 operaciones en 29 dominios. 72 servidas siempre, 41 tras el interruptor de escritura y 12 en la superficie de gasto.
- Referencia interactiva: /docs/reference · catálogo completo: /docs/operations
1. Antes de empezar: qué plan necesitas
Emitir una API key es en sí mismo una función de pago. Requiere la capacidad manage_api_keys, y
los planes gratuitos no la traen:
| Plan | ¿Puede emitir API keys? |
|---|---|
| Free / PAYG | No. No hay pantalla de claves. |
Starter (starter, pro, telephony-pilot) | Sí |
| Business | Sí |
| Agency · Partner · Enterprise | Sí |
Si estás en Free o PAYG y no ves la sección de API Keys en el panel, no es un fallo: es el plan. Habla con tu distribuidor.
2. Emitir tu API key
- Entra en el panel → API Keys (
/app/api-keys). - Crea la clave. Se enseña una sola vez: se guarda hasheada y no hay forma de volver a verla. Si la pierdes, emites otra.
- Guárdala en una variable de entorno. Nunca en el repositorio, nunca en código que llegue al navegador.
export AMAI_API_KEY="amai_..."
export AMAI_API_HOST="voice.amai.run" # el tuyo, si contrataste con un revendedorLa clave no caduca por tiempo y es revocable al instante desde el mismo panel. Revocarla corta el acceso en la siguiente petición.
3. Tu primera llamada (60 segundos)
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/calls?limit=1"Si responde 200, ya está: la clave es válida, tiene permiso y tu plan incluye la función.
Si responde otra cosa, la sección 5 te dice exactamente cuál de las tres puertas te ha parado.
El tenant sale siempre de la clave. No existe ningún parámetro para consultar los datos de otra cuenta, ni en esta operación ni en ninguna. Es el único control de aislamiento que hay, y por eso no es negociable.
4. La triple puerta
Cada petición atraviesa tres controles independientes. Los tres tienen que decir que sí — con una salvedad sobre el segundo, que está en despliegue y se explica al final de esta sección:
petición → ① ¿la clave es válida? → 401 si no
→ ② ¿la clave tiene el scope? → 403 insufficient_scope
→ ③ ¿el plan incluye la función? → 403 capability_required
→ 200- La clave. Existe, no está revocada, y llega en
Authorization: Bearer amai_…. UnBearerque no sea una clave válida da401— nunca cae de vuelta a la sesión del navegador, para que una clave muerta no parezca viva. - El scope. Cada operación exige un permiso concreto (
calls:read,whatsapp:send…). Los permisos se conceden al emitir la clave. Catálogo completo en la sección 14. - La capacidad del plan. Es una propiedad de la cuenta, no de quien llama. La misma cuenta no puede tener una función por cookie y no tenerla por clave: se comprueba igual en los dos caminos, y sin caché, para que quitar una función corte el acceso de inmediato.
Y hay una cuarta puerta, solo para las operaciones que gastan dinero. Está en la sección 7.
Hasta dónde llega hoy la puerta ②
La puerta ② está en despliegue, y conviene saber dónde deniega ya:
- Deniega hoy en la superficie de escritura, en la de gasto y en la Developer
Capsule. Y en estas lecturas: contactos y listas de contactos (listado y ficha), la
ficha de una campaña y sus listas, y la ficha de una subcuenta con su marca y su
empaquetado. También en las cuatro lecturas nuevas del historial de campaña: sus
inscripciones, sus intentos, los veredictos de cualificación y las citas.
Ahí, una clave sin el scope recibe
403 insufficient_scopey no pasa. Ojo: el listado de campañas y el listado de subcuentas todavía no, aunque sus fichas sí. - En el resto de las lecturas —y en las rutas de mensajería que llevan su propio preámbulo— el scope exigido se registra en el uso de la clave, pero todavía no deniega. Una clave válida de la cuenta llega a la operación aunque no lleve ese permiso declarado.
Lo que sí protege esas operaciones hoy son las puertas ① y ③: la clave tiene que ser válida y viva, y la función tiene que estar en el plan de la cuenta.
Qué significa para ti. Si emites una clave para un tercero y necesitas que el recorte de permisos sea efectivo ahora, no te apoyes solo en el scope: el control que ya muerde para todas las operaciones es la capacidad de la cuenta (sección 15), y quien la concede es tu distribuidor. Declara igualmente los scopes al emitir la clave: son los que empezarán a denegar cuando la puerta ② termine de desplegarse, y hacerlo hoy evita que ese día se te caiga una integración.
5. Los dos 403, y cómo distinguirlos
Es la confusión que más tiempo cuesta, así que va con su propia sección. Los dos son 403, los dos
dicen «no puedes», y se arreglan en sitios distintos:
{ "error": { "code": "insufficient_scope", "message": "..." } }→ Le falta el permiso a la clave. Lo arregla quien emitió la clave: emite otra con ese
scope. Cambiar de plan no sirve de nada. Ojo al despliegue de la puerta ② (sección 4): hay
operaciones que todavía no devuelven este 403 aunque a la clave le falte el permiso.
{ "error": { "code": "capability_required", "message": "Tu plan no incluye esta función (\"...\")..." } }→ El plan de la cuenta no incluye la función. Emitir una clave nueva no cambia nada: daría
el mismo 403. Lo arregla el plan contratado, o un permiso explícito que tu distribuidor puede
conceder a tu cuenta.
Ramifica siempre por error.code, nunca por el 403 a secas.
6. El 404 que no es culpa tuya
Las operaciones de escritura y de gasto viven detrás de interruptores de despliegue. Apagados,
responden 404 — exactamente igual que si la ruta no existiera. Es deliberado: un endpoint
apagado no debe revelar que existe.
| Puerta | Qué exige | Operaciones |
|---|---|---|
| Siempre | Nada | 72 |
| Escritura | PUBLIC_API_WRITE_ENABLED | 41 |
| Gasto | PUBLIC_API_WRITE_ENABLED y PUBLIC_API_MONEY_ENABLED y alta de la cuenta y Idempotency-Key | 12 |
Consecuencia práctica: si una operación de escritura te da 404 con un identificador que sabes
bueno, no lo depures como bug tuyo. Pregunta si la superficie está encendida en tu entorno.
Cada operación del catálogo dice a qué puerta pertenece.
7. Operaciones de gasto: la cuarta puerta, y la idempotencia
Las 12 operaciones que mueven dinero o generan tráfico real (llamar, despachar una campaña, recargar saldo, contratar, comprar troncales) tienen dos condiciones más que el resto:
El alta de la cuenta. Encender el interruptor global dice «este entorno puede»; no dice «este
cliente quiso». Sin el alta explícita, la respuesta es 403 money_opt_in_required. No lo arregla
ni una clave nueva ni un plan superior: es un consentimiento por cuenta.
La cabecera Idempotency-Key, obligatoria. Sin ella, 400 missing_idempotency_key.
KEY=$(uuidgen)
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{"to":"+34600111222","agent_id":"<uuid>"}' \
"https://$AMAI_API_HOST/api/v1/calls/dial"La regla que te ahorra un cobro doble: guarda la clave y repítela en cualquier reintento.
| Situación | Respuesta | Qué significa |
|---|---|---|
| Reintento con la misma clave y el mismo cuerpo, ya terminado | El resultado guardado, con Idempotency-Replayed: true | No se ha vuelto a ejecutar. |
| Reintento mientras la primera sigue en vuelo | 409 idempotency_in_progress | Espera y reintenta con la MISMA clave. |
| Misma clave con otro cuerpo | 409 idempotency_conflict | Una clave por operación. Usa una nueva. |
503 idempotency_unavailable | El registro no estaba disponible | No se ha cobrado nada. Reintenta con la misma clave. |
500 internal_error en una operación de gasto | Fallo por nuestro lado | Reintenta con la MISMA clave: te devolverá el mismo 500 en vez de arriesgar un segundo cobro. |
Si cambias la clave en un reintento, la operación se ejecuta dos veces. Ese es todo el riesgo, y es todo evitable.
8. Paginación
Hay dos familias de paginación, y conviene saber en cuál estás antes de escribir el bucle.
8.1 · limit/offset — el resto de la API
Las listas paginan con limit y offset, con los mismos límites:
limit— por defecto 50, máximo 500. Fuera de rango:400 invalid_field.offset— por defecto 0, entero ≥ 0.
La respuesta trae un bloque pagination:
{ "pagination": { "limit": 50, "offset": 0, "total": 412, "has_more": true } }Recorre mientras has_more sea true, sumando limit al offset. No te fíes de que una
página venga corta para parar: usa has_more.
OFFSET=0
while :; do
PAGE=$(curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/calls?limit=500&offset=$OFFSET")
echo "$PAGE" | jq -c '.data[]'
[ "$(echo "$PAGE" | jq -r '.pagination.has_more')" = "true" ] || break
OFFSET=$((OFFSET + 500))
doneUn detalle que ahorra un malentendido: donde hay un bloque summary, es del rango filtrado
completo, no de la página. Aunque pidas limit=1, los totales siguen siendo los del periodo.
8.2 · Cursor — la sección Conversations
/api/v1/conversations, sus mensajes y /api/v1/whatsapp/connections no tienen offset, y
no es un descuido: una bandeja se reordena mientras la lees, así que paginar por posición se salta
hilos y repite otros, en silencio y con un 200 delante.
limit— por defecto 50, máximo 200 (aquí no es 500).cursor— se copia tal cual depage.next_cursor. Es opaco: no lo interpretes, y no lo reutilices entre listas distintas (te responde400).- No hay
total: contar un conjunto que se mueve da un número ya falso cuando lo lees.
{ "page": { "limit": 50, "has_more": true, "next_cursor": "eyJ2IjoxLC..." } }CURSOR=""
while :; do
PAGE=$(curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/whatsapp/connections?limit=50${CURSOR:+&cursor=$CURSOR}")
echo "$PAGE" | jq -c '.data[]'
[ "$(echo "$PAGE" | jq -r '.page.has_more')" = "true" ] || break
CURSOR=$(echo "$PAGE" | jq -r '.page.next_cursor')
doneLa condición de parada es page.has_more, en las tres listas, y se calcula leyendo una fila
de más — no se deduce del tamaño de la página.
9. Filtros y fechas
- Las fechas son ISO-8601:
2026-07-01o2026-07-01T00:00:00Z. Nada relativo, nada local. - Los rangos son inclusivos por los dos extremos:
end_date=2026-07-31incluye el 31 entero. - Los enumerados se validan de forma estricta. Un valor no reconocido es
400, nunca un filtro ignorado en silencio: un filtro que se ignora devuelve datos de más y nadie se entera. - Un identificador que no tenga forma de UUID es
400 invalid_id_formatantes de tocar la base de datos.
10. Límites de peticiones
| Límite | Presupuesto | Cómo se cuenta |
|---|---|---|
| Admisión | 600 / min | Por IP, antes de mirar la clave. Solo existe para que una avalancha anónima no llegue a la base de datos. |
| Por clave | 60 / min | Por key_id. Este es el que importa. |
El presupuesto que cuenta va por clave, no por IP: rotar de dirección no te da cuota nueva, y el tráfico de un cliente nunca gasta el presupuesto de otro.
Al pasarte recibes 429 con las cabeceras Retry-After y X-RateLimit-Reset, ambas en
segundos. Respétalas: reintentar antes solo consume el presupuesto siguiente.
11. Los dos sobres de error
Esta API responde los errores con dos formas distintas, y hay que saberlo antes de escribir el manejador:
{ "error": { "code": "invalid_field", "message": "..." } }Es el sobre tipado, el de toda la superficie nueva.
{ "error": "member_not_found" }Es el sobre legado — error como cadena —, y lo devuelven las 9
operaciones más antiguas, que ya tienen clientes en producción cuyo parseo no controlamos:
GET /api/v1/calendarGET /api/v1/calendar/checkPUT /api/v1/calls/{call_id}/memberDELETE /api/v1/calls/{call_id}/memberPOST /api/v1/contacts/identifyPOST /api/v1/email/sendGET /api/v1/membersGET /api/v1/team/mention-listGET /api/v1/whatsapp/messages
Una línea cubre las dos, para siempre:
const code = typeof body.error === "string" ? body.error : body.error?.codeY normaliza a minúsculas antes de comparar: en el sobre legado conviven unauthorized,
Unauthorized y UNAUTHORIZED para el mismo fallo.
12. Tabla completa de errores
Todos los códigos que la API puede devolver, con qué hacer ante cada uno. Se extrae del código fuente, no de la memoria de nadie: hay un test que se pone rojo si una ruta empieza a devolver un código que no esté en esta tabla.
| HTTP | Código | Sobre | Qué ha pasado | Qué hacer |
|---|---|---|---|---|
| 400 | agent_not_editable | tipado | El agente existe pero su tipo no admite edición por API. | Edítalo desde el panel. No hay ruta de API para ese tipo de agente. |
| 400 | batch_too_large | tipado | El lote enviado supera el máximo de elementos por petición. | Trocea el lote. El máximo exacto viene en el mensaje del error. |
| 400 | body_too_long | tipado | El cuerpo del SMS supera los 1530 caracteres (≈10 segmentos GSM). | Recorta el mensaje. Un SMS se factura por segmentos de 153 caracteres: 1530 es el tope que la plataforma acepta en un solo envío. |
| 400 | channel_unavailable | tipado | La cuenta no tiene proveedor de SMS configurado y activo. | Configura el canal de SMS en el panel (proveedor, remitente y credenciales) antes de llamar a esta operación. |
| 400 | country_not_supported | tipado | El país del destinatario no está en la lista de países habilitados para esta cuenta. | Amplía allowed_countries en la configuración de SMS del panel, o manda a un número de un país ya habilitado. |
| 400 | domain_not_set | tipado | Se ha pedido verificar el dominio de una cuenta que no tiene ninguno guardado. | Guárdalo primero con PATCH …/branding. Esa respuesta trae el registro TXT que hay que publicar; la verificación NO acepta un dominio en el cuerpo, a propósito. |
| 400 | duplicate_day | tipado | El horario enviado repite el mismo día de la semana. | Manda un único bloque por día. Si necesitas dos tramos, usa el rango del mismo bloque. |
| 400 | event_not_emitted | tipado | Los eventos de webhook que pediste existen en el catálogo pero no los emite nada: ningún punto del sistema los dispara. No es un fallo de tu integración, y por eso no es invalid_field — el nombre estaba bien escrito. | Quítalos de events. El mensaje enumera los que sí se entregan hoy (call.ended, call.failed, agent.created, whatsapp.message.received). Un evento que nadie emite te dejaría esperando un aviso que no llega nunca. |
| 400 | fixed_line | tipado | El destinatario es un número FIJO, y un SMS a un fijo no llega a ninguna parte. | Manda a un móvil. La clasificación la hace libphonenumber-js/max sobre el E.164, no hay forma de forzarla. |
| 400 | invalid_body | tipado | El cuerpo se ha parseado pero no es un objeto JSON. | Manda un objeto ({...}). Un array o un escalar en la raíz no se aceptan. |
| 400 | invalid_date | los dos | Una fecha no es ISO-8601 válida. | Usa 2026-07-01 o 2026-07-01T00:00:00Z. Nada de fechas relativas ni locales. |
| 400 | invalid_default_country | tipado | default_country no es un código de país ISO-3166 alpha-2 reconocido (por ejemplo ES, FR, MX). | Manda el código de dos letras del país, o omite el campo: solo hace falta para los números que llegan sin prefijo internacional, y si tu cuenta tiene un único país habilitado se usa ése automáticamente. |
| 400 | invalid_domain | tipado | custom_domain no tiene forma de dominio después de normalizarlo (se le quitan el esquema, la ruta y las mayúsculas). | Manda solo el host: panel.miempresa.com, no https://panel.miempresa.com/login. Para quitar el dominio, manda null. |
| 400 | invalid_field | tipado | Un campo tiene un valor fuera de rango o de tipo equivocado. Es el error más común: también lo devuelven limit y offset cuando se salen de sus límites. | El mensaje nombra el campo y el rango aceptado. Corrige y reintenta. |
| 400 | INVALID_FILTER | legado | Un filtro de la query string no es válido. | Revisa el nombre y el valor del filtro contra la referencia de la operación. |
| 400 | invalid_id_format | los dos | Un identificador de la ruta o del cuerpo no tiene forma de UUID. | Copia el id tal cual lo devolvió la API. No lo recortes ni lo normalices. |
| 400 | invalid_idempotency_key | tipado | La cabecera Idempotency-Key tiene una longitud o unos caracteres no admitidos. | Usa un UUID v4. El rango de longitud y el alfabeto exacto vienen en el mensaje. |
| 400 | invalid_json | los dos | El cuerpo no es JSON parseable. | Revisa comas y comillas, y manda Content-Type: application/json. |
| 400 | INVALID_JSON | legado | Lo mismo que invalid_json, en una operación del sobre legado. | Idéntica corrección. La diferencia es solo de mayúsculas y de sobre. |
| 400 | invalid_label | tipado | La etiqueta de llamada no cumple el formato admitido. | Usa una clave corta sin espacios. El mensaje detalla el patrón. |
| 400 | invalid_number | tipado | El número de destino no se puede interpretar como E.164 y no tiene país reconocible. | Manda el número en E.164 completo, con prefijo internacional (+34600111222). |
| 400 | invalid_phone | tipado | El número no está en E.164. | Manda +34600111222: prefijo +, país y número, sin espacios ni guiones. |
| 400 | invalid_range | los dos | El rango de fechas es incoherente (fin anterior al inicio) o excede el máximo. | Ordena las fechas y trocea el periodo si el mensaje indica un máximo. |
| 400 | invalid_recipient | tipado | Lo mismo que INVALID_RECIPIENT, en el sobre tipado. | Para SMS, un móvil en E.164 de un país habilitado en la cuenta. |
| 400 | INVALID_RECIPIENT | legado | El destinatario no es válido para el canal elegido. | Para email, una dirección; para SMS y WhatsApp, un E.164. |
| 400 | INVALID_REFERENCE | legado | contact_id o call_id no tienen forma de UUID. Se comprueba ANTES de enviar: esas dos referencias van a columnas uuid, y una mal formada tumbaba el registro DESPUÉS de que el correo ya hubiera salido — se enviaba, no quedaba constancia, y la respuesta seguía siendo 200. | Manda el UUID que devuelve la propia API (call_GG... no vale aquí: es el id público; usa el uuid interno) u omite el campo. |
| 400 | invalid_request | tipado | La petición se rechazó por una regla de negocio que no tiene código propio publicado. En la práctica no debería verse: existe para que una regla nueva se rechace con un código genérico en vez de publicar un contrato que nadie ha documentado. | Lee el campo message. Si aparece de forma reproducible, repórtalo: significa que falta un código en esta tabla. |
| 400 | invalid_thresholds | tipado | Los umbrales de aviso no forman una secuencia válida. | Manda umbrales crecientes y dentro del rango que indica el mensaje. |
| 400 | invalid_time | legado | Una hora del horario laboral no tiene formato HH:MM. | Usa 24 h con dos dígitos: 09:00, 18:30. |
| 400 | invalid_url | tipado | El destino del webhook no es una URL aceptable: no se parsea, no usa http:// ni https://, o apunta —o resuelve por DNS— a una dirección de bucle local, privada, de enlace local o de metadatos de nube. También se rechaza si nuestro resolutor no puede comprobar a dónde apunta el dominio: se falla cerrado a propósito. | Registra una URL alcanzable desde Internet. Se comprueba al dar de alta —y otra vez en cada entrega y en cada redirección—, así que un destino aceptado aquí es un destino que de verdad se puede entregar: un 200 OK al registrar un http://localhost/ sería una integración muerta que parece viva. |
| 400 | missing_contact_ids | tipado | La operación necesita al menos un contacto y no llegó ninguno. | Manda contact_ids con al menos un identificador. |
| 400 | missing_field | tipado | Falta un campo obligatorio del cuerpo. | El mensaje nombra el campo. Añádelo. |
| 400 | MISSING_FIELDS | legado | Lo mismo que missing_field, en una operación del sobre legado. | Idéntica corrección. |
| 400 | missing_first_name | tipado | Un contacto llega sin nombre de pila. | first_name es obligatorio al crear un contacto. |
| 400 | missing_idempotency_key | tipado | Una operación de gasto llegó sin cabecera Idempotency-Key. Es obligatoria en TODA la superficie de gasto. | Genera un UUID por operación, mándalo en Idempotency-Key y repítelo en los reintentos: es lo único que garantiza que un timeout no te cobre dos veces. |
| 400 | missing_name | tipado | Falta el nombre del recurso que se está creando. | Manda name. |
| 400 | missing_phone | tipado | Falta el teléfono en un recurso que lo exige. | Manda el número en E.164. |
| 400 | MISSING_QUERY | legado | Falta el parámetro de búsqueda obligatorio. | Añade q (o el parámetro que indique la referencia de esa operación). |
| 400 | missing_template_name | tipado | Un envío de WhatsApp de tipo plantilla llegó sin nombre de plantilla. | Manda template_name con una plantilla aprobada por Meta. |
| 400 | missing_text | tipado | Un envío de WhatsApp de texto llegó sin cuerpo. | Manda text. |
| 400 | missing_to | tipado | Un envío de WhatsApp llegó sin destinatario. | Manda to en E.164. |
| 400 | no_active_connection | tipado | El tenant no tiene ninguna conexión de WhatsApp activa. | Conecta WhatsApp desde el panel. No se arregla con la clave ni con el plan: no hay canal por el que enviar. |
| 400 | no_fields | tipado | Un PATCH llegó sin ningún campo modificable. | Manda al menos un campo editable. Un PATCH vacío se rechaza en vez de responder 200 sin hacer nada — un 200 mentiría. |
| 400 | not_mobile | tipado | No se ha podido demostrar que el destinatario sea un móvil. Cubre los rangos que la numeración mundial no clasifica (fijo-o-móvil, VoIP, y todo Estados Unidos y Canadá, donde la portabilidad destruyó la distinción). | Verifica que el número es móvil por otra vía. La plataforma no envía sin prueba explícita: un SMS a un fijo se cobra y no llega. |
| 400 | pricing_not_configured | tipado | El país del destinatario no tiene tarifa en la configuración de SMS de tu cuenta, así que el mensaje no se ha enviado. No es un fallo del proveedor: es un rechazo nuestro, ANTES de tocar el cable. allowed_countries y pricing_per_country son dos listas distintas, y basta con que un país esté en la primera y falte en la segunda. | Añade el país a pricing_per_country en la configuración de SMS del panel. Reintentar sin eso da siempre lo mismo. El intento queda registrado en el histórico de SMS con estado failed, así que puedes ver desde cuándo pasa. |
| 400 | queue_label_not_allowed | tipado | Se intentó escribir a mano una etiqueta reservada al sistema de colas. | Usa otra clave de etiqueta. Las de cola las gestiona la plataforma. |
| 400 | range_too_large | legado | El rango de fechas pedido excede el máximo de esa operación. | Trocéalo en ventanas más cortas y concaténalas tú. |
| 400 | too_many_contacts | tipado | La importación supera el máximo de contactos por petición. | Divide el fichero. El máximo exacto viene en el mensaje. |
| 400 | too_many_members | legado | Se pidieron más miembros de los que la operación devuelve de una vez. | Baja limit y pagina. |
| 400 | too_many_recipients | tipado | POST /api/v1/sms/check recibió más de 500 números en un solo lote. Es el mismo tope que las otras operaciones de lote de la API. | Parte la lista en trozos de 500. El veredicto de cada número es independiente de los demás, así que trocear el lote no cambia ningún resultado. |
| 401 | unauthenticated | legado | Lo mismo que unauthorized, en una operación del sobre legado. | Idéntica corrección. |
| 401 | unauthorized | los dos | Falta la credencial, o la clave no existe, está revocada o no es válida. También sale si mandas un Bearer que no es una API key de AMAI. | Manda Authorization: Bearer amai_<key>. Si la clave es tuya y sale 401, está revocada: emite otra en el panel. Un Bearer inválido nunca cae a la sesión del navegador — es deliberado, para que una clave muerta no parezca viva. |
| 401 | Unauthorized | legado | Lo mismo que unauthorized, con otra capitalización, en el sobre legado. | Idéntica corrección. Compara el código en minúsculas si vas a ramificar por él: la capitalización no es estable entre las operaciones antiguas. |
| 401 | UNAUTHORIZED | legado | Tercera capitalización del mismo fallo, en el sobre legado. | Idéntica corrección. Normaliza a minúsculas antes de comparar. |
| 402 | insufficient_balance | tipado | La cuenta no tiene saldo suficiente para la operación de gasto que has pedido. | Recarga saldo y reintenta. Con una Idempotency-Key nueva: la anterior quedó asociada a un intento que no se ejecutó. |
| 403 | capability_required | tipado | El plan de la cuenta no incluye esta función. No es un problema de la clave: la misma clave con todos los scopes del mundo seguiría dando 403. | Lo arregla el plan contratado. Contacta con tu distribuidor para habilitar la función en la cuenta. El mensaje nombra la capacidad exacta entre comillas. |
| 403 | domain_not_allowed | tipado | El dominio propio es una función de plan Partner o superior, y el plan que se mira es el de la cuenta a la que se le pone, no el de quien llama. | Sube el plan de esa cuenta, o quita custom_domain del cuerpo. La lectura de la marca publica custom_domain_allowed justamente para saberlo antes de intentarlo. |
| 403 | forbidden | legado | Denegación genérica de una operación del sobre legado. | Revisa el scope de la clave y la capacidad del plan. Estas operaciones antiguas no distinguen las dos causas — es la razón por la que el sobre nuevo sí. |
| 403 | insufficient_permission | tipado | Solo con la sesión del panel. Tu USUARIO no tiene el permiso que la operación exige. Es la tercera pregunta, distinta de las otras dos: el scope dice qué puede hacer una clave, el plan dice qué ha contratado la cuenta, y esto dice qué puedes hacer TÚ. Aplica por igual a escritura y a lectura: en este producto ver también es un permiso, así que un miembro sin billing.view recibe este error en GET /api/v1/billing/* aunque su cuenta tenga la función contratada — exactamente como el panel le esconde la sección. Nunca aparece cuando llamas con Authorization: Bearer amai_…: una clave no es una persona y no tiene rol. | Lo arregla un administrador de tu organización, asignándote un rol que incluya el permiso. El mensaje nombra los permisos exactos que faltan (p. ej. agents.edit). |
| 403 | insufficient_scope | los dos | La clave es válida pero no lleva el permiso de esta operación. | Lo arregla quien emitió la clave: hay que emitir una nueva con ese scope. Cambiar de plan no sirve de nada aquí. |
| 403 | INSUFFICIENT_SCOPE | legado | Lo mismo que insufficient_scope, en el sobre legado. | Idéntica corrección. |
| 403 | money_opt_in_required | tipado | La cuenta no tiene habilitadas las operaciones de gasto por API. Es un tercer permiso, distinto del scope y del plan: un consentimiento explícito por cuenta. | Pídeselo a tu distribuidor. Ni una clave nueva ni un plan superior lo activan solos. |
| 403 | permission_model_gap | tipado | Solo con la sesión del panel. La operación no tiene un permiso de usuario equivalente en el modelo, así que no se puede autorizar por cookie — ni siquiera al propietario de la cuenta. Se cierra en vez de dejar pasar. Hoy afecta a kb:write, calls:dial y kb:read, cuyo control en el panel es un gate de plan y no un permiso por persona. | Llama con una API key que tenga ese scope. No hay ningún rol que puedas pedir: el permiso no existe. |
| 403 | recipient_suppressed | tipado | El destinatario pidió no ser contactado. Está en la lista de exclusión de tu cuenta —por una baja, una petición en una llamada, o la Lista Robinson— o es un contacto con opt-out. Es una regla sobre la PERSONA, no sobre el mensaje: reintentar con el mismo número no lo arregla, y mandarlo igual sería la infracción que este error evita. | No reintentes. Si de verdad esa persona ha vuelto a dar su consentimiento, hay que levantar su exclusión primero — y eso deja su propio rastro, con quién y por qué. |
| 403 | RECIPIENT_SUPPRESSED | legado | El destinatario pidió no recibir correo de esta cuenta. No es un problema de la clave ni del plan: es la voluntad de una persona, y gana a cualquier permiso. El cuerpo trae reason con la fuente exacta de la negativa — contact_do_not_email (pulsó el enlace de cancelación), contact_opt_out, suppression_entry o exclusion_list (lo dijo en una llamada y quedó registrado). El correo NO se ha enviado. | No reintentes. Si la negativa es un error, hay que levantarla desde el panel, con autor y motivo; eso es una decisión de un humano de tu organización, no una corrección de código. Y si tu integración envía en lote, sáltate a esta persona en vez de reintentarla: cada reintento es un envío que la ley te prohíbe hacer. |
| 403 | self_not_allowed | tipado | La operación se puede ejercer sobre una cuenta del árbol, pero no sobre la propia. Hoy solo la escritura de verjas de empaquetado. | Apunta a una subcuenta. Las verjas de TU cuenta las fija quien te sirve la plataforma: poder reescribirlas desde dentro desharía el acuerdo comercial que las puso. |
| 404 | call_not_found | legado | La llamada no existe en tu tenant. | Comprueba el call_id contra GET /api/v1/calls. |
| 404 | member_not_found | legado | El miembro no existe en tu tenant. | Comprueba el member_id contra GET /api/v1/members. |
| 404 | No organization found | legado | La misma causa que no_organization, con el texto en prosa inglesa. | Idéntica corrección. Este literal es la mejor prueba de por qué conviene normalizar el código antes de ramificar por él. |
| 404 | no_organization | los dos | La credencial no resuelve a ningún tenant activo. | La cuenta puede estar suspendida. Contacta con tu distribuidor. |
| 404 | not_found | tipado | El recurso no existe en tu tenant, o la operación está apagada por interruptor de despliegue. Las dos causas responden igual, y es a propósito. | Comprueba el id. Si es correcto y sigues viendo 404 en una operación de escritura o de gasto, la superficie está apagada en tu entorno: pregúntanos, no lo depures como bug tuyo. |
| 409 | already_linked | legado | El recurso ya está asociado a lo que intentas asociarlo. | No hace falta hacer nada: el estado que buscabas ya es el actual. |
| 409 | domain_taken | tipado | Ese dominio propio ya está asignado a otra cuenta. | Elige otro. No es un choque de nombres: la plataforma resuelve el inquilino por host con una consulta que espera una fila, así que dos cuentas con el mismo dominio pierden las dos su marca a la vez — y con ella su allow_signup. |
| 409 | duplicate_contact | tipado | Ya existe un contacto con ese teléfono o email en tu tenant. | Recupéralo y haz PATCH en vez de POST. |
| 409 | idempotency_conflict | tipado | Esa Idempotency-Key ya se usó en la cuenta con otro cuerpo. | Usa una clave nueva. Una clave por operación, y solo se repite para reintentar EXACTAMENTE la misma. |
| 409 | idempotency_in_progress | tipado | Otra petición con esa misma clave sigue ejecutándose. | Espera unos segundos y reintenta con la misma clave. Si la cambias, la operación se ejecutará dos veces. |
| 409 | journey_stopped | tipado | El recorrido de ese contacto ha parado el canal de WhatsApp: reservó una cita, se convirtió, o pidió que dejáramos de escribirle. El mensaje no se ha enviado y no ha quedado registrado ningún intento. | No se arregla reintentando, y por eso no es un 5xx. La parada es un hecho sobre la persona, no un fallo nuestro: si de verdad hay que escribirle (confirmarle la cita, moverla), hazlo desde la bandeja del panel — una persona escribiendo a mano sí pasa, porque el humano siempre gana. La automación no. |
| 409 | suppression_lift_denied | tipado | El PATCH intentaba deshacer una baja: poner do_not_call/opt_out a false, o sacar status de do_not_call, sobre un contacto que está dado de baja. La baja sigue intacta. | No se levanta por API. Levantar una baja exige motivo y autor identificable, y se hace desde el panel (POST /api/app/contacts/{id} con sesión). Por API sólo se puede PONER la baja, que es la dirección que no puede hacer daño. |
| 410 | target_not_active | legado | El destino existe pero ya no está activo (por ejemplo, un miembro dado de baja). | Elige otro destino. Reintentar sobre el mismo no va a cambiar nada. |
| 413 | attachment_too_large | tipado | El adjunto supera el máximo por petición (15 MiB). El mensaje trae el tamaño real y el límite, en bytes. | No hay reintento que lo arregle: el fichero no se puede descargar por esta vía. Su ficha (nombre, tipo, tamaño) sigue disponible en el listado de adjuntos del mensaje. |
| 422 | outside_24h_window | tipado | Intentas mandar texto libre por WhatsApp fuera de la ventana de 24 h desde el último mensaje del usuario (código de Meta 131047). | Manda una plantilla aprobada (type=template). Es la única vía fuera de la ventana. |
| 429 | rate_limited | tipado | Has superado un límite de peticiones. Hay dos: uno de admisión por IP (600/min) y el que de verdad importa, 60/min por clave. | Respeta la cabecera Retry-After (segundos). No rotes de IP para esquivarlo: el presupuesto que cuenta va por clave, no por dirección. |
| 500 | Error interno | legado | La misma causa que internal_error, con el texto en prosa castellana. | Idéntica conducta. |
| 500 | Internal server error | legado | La misma causa que internal_error, con el texto en prosa inglesa. | Idéntica conducta. |
| 500 | internal_error | los dos | Fallo no controlado por nuestro lado. | Reintenta con espera exponencial. Si era una operación de gasto, reintenta con la misma Idempotency-Key: te devolverá el mismo 500 en vez de volver a intentar un cobro cuyo estado no conocemos. Si persiste, escríbenos con la hora y la ruta. |
| 502 | meta_error | tipado | Meta (WhatsApp) ha rechazado la operación. El cuerpo incluye meta_code con el código original de Meta. | Busca el meta_code en la documentación de Meta. Reintentar sin cambiar nada rara vez ayuda: casi siempre es un problema de plantilla, de ventana o de número. |
| 502 | provider_error | tipado | El proveedor de SMS rechazó el envío. La respuesta trae message_id: la fila de auditoría que la plataforma ha dejado igualmente, con el código y el motivo del proveedor guardados. | Consulta esa fila en GET /api/v1/sms/history para ver error.code y error.message del proveedor. El texto crudo no se devuelve aquí porque es de un tercero y puede llevar detalle de la cuenta. |
| 502 | upstream_error | tipado | Un tercero del que depende la operación no respondió o respondió mal. Hoy lo devuelve el archivo documental (GET /api/v1/documents). | Reintenta en unos minutos. No cambies la petición: el fallo no está en lo que mandaste. |
| 503 | connection_lookup_failed | tipado | POST /api/v1/whatsapp/check no pudo comprobar si el tenant tiene una conexión de WhatsApp activa. Fail-closed: sin saber si hay conexión, no se puede emitir ningún veredicto de contactabilidad. | Es transitorio. Reintenta en unos segundos. Si persiste, es una avería nuestra y no de tu petición. |
| 503 | idempotency_unavailable | tipado | El registro de idempotencia no está disponible. La operación no se ha ejecutado y no se ha cobrado nada. | Reintenta con la MISMA Idempotency-Key. Es seguro: el efecto nunca llegó a correr. |
| 503 | integration_not_configured | tipado | El plan SÍ incluye la función, pero la cuenta no tiene conectada la integración que la sirve. Es deliberadamente distinto del 403: un 403 mandaría a pedir un cambio de plan que no arreglaría nada. | No lo arregla la clave ni el plan: contacta con tu distribuidor para que conecte la integración de la cuenta. |
| 503 | recados_source_unavailable | tipado | La fuente de recados no responde. | Reintenta más tarde. No es un error de tu petición. |
| 503 | recipient_suppression_unverifiable | tipado | No se pudo comprobar si el destinatario pidió no ser contactado. La lectura de la lista de exclusión falló, y ante la duda no se envía. Es un fail-CLOSED deliberado: el coste de no mandar un mensaje es que llega tarde; el de mandarlo a quien dijo que no es una multa. | Es transitorio. Reintenta en unos segundos. Si persiste, es una avería nuestra y no de tu petición: el mismo cuerpo funcionará en cuanto la lectura vuelva. |
| 503 | search_timeout | tipado | La búsqueda ha excedido el tiempo máximo. | Acota la consulta (menos rango, más filtros) y reintenta. |
| 503 | send_not_recorded | tipado | No se pudo registrar el envío antes de mandarlo. No se ha enviado nada. | Reintenta. Es un fallo transitorio de la base de datos, no de tu petición: la plataforma prefiere no enviar a enviar sin dejar registro. |
| 503 | SUPPRESSION_UNVERIFIABLE | legado | No se pudo consultar la lista de supresión, así que el correo NO se ha enviado. Ante la duda no se escribe a nadie: un envío a quien pidió no recibirlo no se puede deshacer, y no enviarlo sí se puede reintentar. El fallo es nuestro y es temporal. | Reintenta. Si persiste, avísanos: significa que la base de datos de supresión no está respondiendo, no que tu petición tenga nada mal. |
13. Ejemplos por dominio
Un curl copiable por cada uno de los 29 dominios. Exportadas AMAI_API_KEY y
AMAI_API_HOST (sección 2), todos funcionan tal cual.
Agentes
Listar los agentes de voz del tenant, ver uno y editar su contenido (saludo, prompt, voz). Crear y borrar agentes NO está abierto por API: se hace desde el panel. 4 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/agents?limit=10"Analítica
Los mismos agregados que pinta el panel: el resumen del dashboard y las estadísticas de llamadas por periodo. 2 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/analytics/dashboard?start_date=2026-07-01&end_date=2026-07-31"Anuncios
Los comunicados internos de la cuenta: los mismos que ve el equipo en el panel. Solo lectura por API — publicarlos y retirarlos se hace desde el panel. 2 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/announcements?limit=20"Auditoría
El registro de actividad del tenant: quién hizo qué y cuándo. 1 operación.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/audit-log?limit=50"Avisos
Configurar, silenciar o descartar los avisos del sistema. 1 operación.
curl -sS -X PATCH \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"alert_id":"<uuid>","dismissed":true}' \
"https://$AMAI_API_HOST/api/v1/alerts"Operación de escritura: 404 mientras
PUBLIC_API_WRITE_ENABLEDesté apagado.
Base de conocimiento
Gestionar las colecciones de documentos que consultan los agentes. La LECTURA de esas bases vive en el dominio «Knowledge Base» — son dos etiquetas para la misma cosa, y está anotado como defecto conocido en la ficha de la guía. 1 operación.
curl -sS -X PATCH \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Manual de producto 2026"}' \
"https://$AMAI_API_HOST/api/v1/kb/collections/<collection_id>"Operación de escritura: 404 mientras
PUBLIC_API_WRITE_ENABLEDesté apagado.
Calendario
El horario laboral del tenant y sus excepciones. GET /api/v1/calendar/check responde si un instante concreto cae dentro del horario — es lo que consulta un agente antes de prometer una devolución de llamada. 4 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/calendar/check?at=2026-07-28T10:30:00Z"Campañas
Crear y editar campañas y asociarles listas de contactos. Lanzarlas no está aquí: el despacho gasta saldo y vive en el dominio «Gasto». 8 operaciones.
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Recordatorio julio","agent_id":"<uuid>"}' \
"https://$AMAI_API_HOST/api/v1/campaigns"Operación de escritura: 404 mientras
PUBLIC_API_WRITE_ENABLEDesté apagado.
Canal
Solo para cuentas de tipo agencia o partner: el árbol de subcuentas y sus estadísticas agregadas. 2 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/agency/stats"Una cuenta cliente recibe
403 capability_requiredaquí, y es correcto: no tiene canal.
Claves de API
Rotar tu propia credencial por programa: se emite una nueva con el mismo nombre y los mismos permisos, y la anterior queda revocada en el acto — sin periodo de gracia, porque se rota cuando una clave se ha filtrado. key_id tiene que ser el id de la clave con la que llamas (el que ves en el panel, no el secreto); cualquier otro responde 404, incluidas las demás claves de tu cuenta. El valor nuevo viaja una sola vez, en esa respuesta. Listar, crear y revocar otras claves sigue siendo del panel. 1 operación.
curl -sS -X POST -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/api-keys/$AMAI_API_KEY_ID/rotate"Contactos
El directorio del tenant. POST /api/v1/contacts/identify es el que usa un agente en mitad de una llamada para saber con quién habla. 8 operaciones.
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+34600111222"}' \
"https://$AMAI_API_HOST/api/v1/contacts/identify"Conversations
La bandeja unificada de WhatsApp: hilos, mensajes, cesión humana y los números conectados, sin entrar nunca en Meta ni manejar un token suyo. Las tres listas —hilos, mensajes y números conectados— se paginan por cursor y no por offset, porque una bandeja se reordena mientras se lee y paginar por posición se salta hilos en silencio; la condición de parada es page.has_more, que se calcula leyendo una fila de más. Toda la sección exige el interruptor CONVERSATIONS_API_ENABLED: apagado responde 404. Y POST .../messages registra la intención de enviar, no envía: quien llama al proveedor es la pasarela. 9 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/conversations?status=open&limit=25"Documentos
El archivo documental de la cuenta: listar y filtrar por tipo, etiqueta, texto y rango de fechas. Las fechas son inclusivas por los dos extremos. 1 operación.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/documents?date_from=2026-07-01&date_to=2026-07-31&limit=25"Exige que la cuenta tenga el archivo CONECTADO, no solo el plan que lo incluye. Si el plan lo incluye y la integración no está configurada, responde
503 integration_not_configured— y eso no lo arregla ni la clave ni un cambio de plan, sino tu distribuidor.
Las identidades de envío de correo dadas de alta en la cuenta: desde qué direcciones puede enviar, y cuáles están verificadas. Útil para elegir remitente ANTES de llamar al envío. 1 operación.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/email/connections"Solo lectura, y a propósito: no devuelve ninguna credencial de la conexión (ni contraseña, ni token de OAuth). Dar de alta o revocar una identidad se hace desde el panel.
Equipo
La administración del equipo: invitaciones pendientes, plazas contratadas y ocupadas, y los roles personalizados del tenant. Se puede revocar una invitación. 4 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/team/seats"Las tres lecturas comparten el scope
members:read. La revocación (DELETE /api/v1/team/invites/{invite_id}) es otra autorización —invites:revoke— y además exigePUBLIC_API_WRITE_ENABLED.
Espacio de trabajo
Lo que el panel enseña en su bandeja: recados, avisos y el listado de campañas. Es la vista de lectura del día a día. 3 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/recados?limit=20"Facturación
Consulta de saldo, suscripción, consumo, movimientos y facturas. Todo de LECTURA: contratar y recargar viven en «Gasto». 8 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/billing/balance"Gasto
Todo lo que mueve dinero o genera tráfico real: lanzar una llamada, despachar una campaña, recargar saldo, contratar plan o add-on, crear troncales SIP y gestionar subcuentas. Es la superficie con más condiciones de toda la API — cuatro, y hacen falta las cuatro. 13 operaciones.
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"to":"+34600111222","agent_id":"<uuid>"}' \
"https://$AMAI_API_HOST/api/v1/calls/dial"Guarda el UUID que generes: si la petición da timeout, reintenta con el mismo o pagarás dos llamadas. Ver la sección de idempotencia de la guía de inicio.
Inbox
La bandeja de correo de la cuenta, de fuera adentro: buzones, carpetas de un buzón, mensajes de una carpeta, un mensaje, sus adjuntos y el hilo al que pertenece. Todo lectura. 7 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/inbox/mailboxes"La descarga de un adjunto corta en 15 MiB con
413 attachment_too_large; por encima de ese tamaño la ficha del adjunto sigue estando en el listado, pero el binario no se sirve por esta vía.
Knowledge Base
Listar las bases de conocimiento y buscar dentro de una. Es la LECTURA; la gestión de colecciones está bajo «Base de conocimiento». 2 operaciones.
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"horario de atención"}' \
"https://$AMAI_API_HOST/api/v1/knowledge-bases/<kb_id>/search"Listas
Listas de contactos: crearlas, renombrarlas y meter o sacar miembros. 7 operaciones.
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Clientes activos"}' \
"https://$AMAI_API_HOST/api/v1/contact-lists"Operación de escritura: 404 mientras
PUBLIC_API_WRITE_ENABLEDesté apagado.
Llamadas
El historial con desglose de coste y los agregados del periodo completo — el sustituto por API del CSV del panel — más las anotaciones sobre una llamada (miembro asignado, etiquetas). 8 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/calls?start_date=2026-07-01&end_date=2026-07-31&limit=50"Guía completa, con paginación de un mes entero:
/docs/calls.
Marca blanca
Dar de alta clientes con tu marca, sin ratón. El alta ya existía (POST /api/v1/agency/subaccounts, que cuelga la cuenta nueva de la tuya); esto es la otra mitad: leer y escribir cómo se ve el panel de esa cuenta —nombre, logo, colores, dominio propio— y las tres verjas que deciden si admite altas (allow_signup) y qué puede contratar (hide_plans, hide_addons). El identificador tiene que colgar de ti; cualquier otro responde 404 sin decirte si existe. 6 operaciones.
curl -sS -X PATCH -H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"company_name":"Mi Marca","primary_color":"#00a86b","custom_domain":"panel.miempresa.com"}' \
"https://$AMAI_API_HOST/api/v1/subaccounts/$AMAI_SUBACCOUNT_ID/branding"
custom_domain_verifiedno se puede escribir. Se gana publicando el registro TXT que devuelve la lectura (_amai-verify.<dominio>→amai-verify=<account_id>) y llamando aPOST …/branding/verify-domain, que lo comprueba contra el DNS. Y cambiar el dominio revoca la verificación en la misma escritura: si no, apuntar el dominio a otro sitio heredaría un sello que no se ha ganado.
Plantillas
Plantillas de email reutilizables en campañas y secuencias. 4 operaciones.
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Bienvenida","subject":"Hola {{first_name}}","body":"<p>Gracias por confiar en nosotros.</p>"}' \
"https://$AMAI_API_HOST/api/v1/email-templates"Operación de escritura: 404 mientras
PUBLIC_API_WRITE_ENABLEDesté apagado.
Secuencias
Secuencias de seguimiento automatizadas: crearlas y editarlas. 5 operaciones.
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Seguimiento a 3 días"}' \
"https://$AMAI_API_HOST/api/v1/sequences"Operación de escritura: 404 mientras
PUBLIC_API_WRITE_ENABLEDesté apagado.
Telefonía
Los números del tenant y las tarifas de venta propias de la cuenta, con el catálogo de países disponibles. 6 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/numbers"
/api/v1/ratesdevuelve tus tarifas de venta, las de tu cuenta. No expone el coste de proveedor de AMAI ni ningún multiplicador.
Utilidades
Las piezas sueltas que un flujo de agente necesita: calendario, envío de email e histórico de email y de SMS. 7 operaciones.
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"cliente@ejemplo.com","subject":"Tu cita","body":"Confirmada para el martes."}' \
"https://$AMAI_API_HOST/api/v1/email/send"Operación de escritura: 404 mientras
PUBLIC_API_WRITE_ENABLEDesté apagado.
Webhooks
Tu suscripción a los eventos de la plataforma —un destino por cuenta— y el registro de cada intento de entrega. El ejemplo es la consulta, que es lo primero que se necesita: hasta ahora se entregaba a ciegas y el único sitio donde se veía si algo había llegado era la pantalla del panel. 5 operaciones.
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
"https://$AMAI_API_HOST/api/v1/webhooks/deliveries?success=false&limit=20"El secreto de firma no viaja en ninguna lectura:
GET /api/v1/webhookssolo dice si lo hay. Se enseña una única vez, en elPOSTque lo emite. Si lo pierdes,rotate_secretemite otro — y las firmas anteriores dejan de validar.
Enviar texto y plantillas, leer el histórico y consultar las plantillas aprobadas. El token de Meta lo custodia y rota AMAI: nunca lo manejas tú. 4 operaciones.
curl -sS -X POST \
-H "Authorization: Bearer $AMAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"+34600111222","type":"text","text":"Hola desde la API de AMAI"}' \
"https://$AMAI_API_HOST/api/v1/whatsapp/send"Guía completa, con la ventana de 24 h y los webhooks:
/docs/whatsapp.
14. Scopes: el catálogo completo
Un scope es recurso:acción. Una concesión recurso:* cubre todas las acciones de ese recurso;
la concesión * cubre todo y solo se da a claves de primera parte.
La columna «Planes que la traen» es la capacidad de serie del plan. Un administrador puede conceder cualquier capacidad a una cuenta concreta, así que «Ninguno de serie» significa «hay que pedirlo», no «imposible».
| Scope | Qué autoriza | Capacidad de tenant | Planes que la traen |
|---|---|---|---|
agency:read | Leer datos de canal y asignaciones (solo agency/partner) | create_subaccounts | Agency, Partner, Enterprise |
agency:write | Crear y gestionar subcuentas (solo agency/partner) | create_subaccounts | Agency, Partner, Enterprise |
agents:read | Listar y ver agentes de voz | view_ai_agents | Todos |
agents:write | Editar el contenido de un agente (prompt, saludo, voz) | edit_ai_agents | Todos |
alerts:read | Leer alertas y avisos del sistema | — (ninguna) | Todos |
alerts:write | Configurar, descartar o silenciar alertas | — (ninguna) | Todos |
analytics:read | Leer el dashboard y las estadísticas de llamadas | — (ninguna) | Todos |
announcements:read | Leer los anuncios internos publicados por el tenant | — (ninguna) | Todos |
api-keys:write | Rotar la propia clave de API | manage_api_keys | Starter, Business, Agency, Partner, Enterprise |
audit:read | Leer el registro de actividad del tenant | view_audit_log | Todos |
billing:read | Consultar saldo, suscripción, consumo y facturas | view_billing | Todos |
billing:write | Contratar plan o add-on y recargar saldo (mueve dinero) | manage_billing | Todos |
branding:read | Leer la marca blanca y las verjas de empaquetado del árbol | manage_descendant_branding | Agency, Partner, Enterprise |
branding:write | Editar la marca blanca y las verjas de empaquetado del árbol | manage_descendant_branding | Agency, Partner, Enterprise |
calendar:read | Leer el calendario laboral y su disponibilidad | — (ninguna) | Todos |
calendar:write | Editar el calendario laboral y sus excepciones | — (ninguna) | Todos |
calls:dial | Lanzar una llamada saliente (gasta saldo) | use_outbound | Todos |
calls:read | Leer el historial de llamadas | — (ninguna) | Todos |
calls:write | Anotar llamadas (miembro asignado, etiquetas, comentarios) | — (ninguna) | Todos |
campaigns:dispatch | Despachar una campaña (gasta saldo) | manage_campaigns | Ninguno de serie |
campaigns:read | Listar campañas, secuencias y plantillas | manage_campaigns | Ninguno de serie |
campaigns:write | Crear y editar campañas, secuencias y plantillas | manage_campaigns | Ninguno de serie |
contacts:read | Listar y buscar contactos | — (ninguna) | Todos |
contacts:write | Crear y editar contactos | — (ninguna) | Todos |
documents:read | Listar el archivo documental del tenant | view_documentos | Ninguno de serie |
email-configs:read | Ver las identidades de envío de email conectadas | send_emails | Ninguno de serie |
email:read | Leer el histórico de email | send_emails | Ninguno de serie |
email:send | Enviar email | send_emails | Ninguno de serie |
inbox:read | Leer los buzones compartidos del tenant (bandeja de entrada) | send_emails | Ninguno de serie |
invites:revoke | Revocar una invitación de equipo pendiente | manage_team | Ninguno de serie |
kb:read | Listar y buscar en la base de conocimiento | use_knowledge_base | Ninguno de serie |
kb:write | Subir y gestionar documentos de la base de conocimiento | manage_knowledge_bases | Ninguno de serie |
members:read | Ver los miembros de la cuenta | manage_team | Ninguno de serie |
numbers:read | Listar los números del tenant | view_telephony | Todos |
numbers:write | Configurar o liberar un número | manage_dids | Todos |
rates:read | Consultar las tarifas propias del tenant | manage_rates | Starter, Business, Agency, Partner, Enterprise |
recados:read | Leer la bandeja de recados | view_recados | Ninguno de serie |
sip-trunks:read | Ver los troncales SIP del tenant | view_sip_trunks | Todos |
sip-trunks:write | Crear y gestionar troncales SIP | create_sip_trunks | PAYG, Starter, Business, Agency, Partner, Enterprise |
sms:read | Leer el histórico de SMS | send_sms | Ninguno de serie |
sms:send | Enviar SMS | send_sms | Ninguno de serie |
subaccounts:read | Ver las subcuentas del árbol (solo agency/partner) | create_subaccounts | Agency, Partner, Enterprise |
team:read | Ver el equipo del tenant | manage_team | Ninguno de serie |
webhooks:read | Ver la suscripción de webhooks y el registro de entregas | view_telephony | Todos |
webhooks:write | Registrar, modificar y eliminar el destino de webhooks | view_telephony | Todos |
whatsapp:read | Leer conversaciones y plantillas de WhatsApp | connect_whatsapp | Todos |
whatsapp:send | Enviar mensajes de WhatsApp | manage_whatsapp | Todos |
15. Qué alcanza cada plan
Una capacidad concedida por el plan ✅ abre todas las operaciones que la exigen. La última columna cuenta cuántas son.
| Capacidad | Free | PAYG | Starter | Business | Agency | Partner | Enterprise | Operaciones |
|---|---|---|---|---|---|---|---|---|
connect_whatsapp | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 6 |
create_sip_trunks | — | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 3 |
create_subaccounts | — | — | — | — | ✅ | ✅ | ✅ | 5 |
edit_ai_agents | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 1 |
manage_api_keys | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | 1 |
manage_billing | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 5 |
manage_campaigns | — | — | — | — | — | — | — | 19 |
manage_descendant_branding | — | — | — | — | ✅ | ✅ | ✅ | 5 |
manage_dids | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 1 |
manage_knowledge_bases | — | — | — | — | — | — | — | 1 |
manage_rates | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | 2 |
manage_team | — | — | — | — | — | — | — | 6 |
manage_whatsapp | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 5 |
send_emails | — | — | — | — | — | — | — | 10 |
send_sms | — | — | — | — | — | — | — | 3 |
use_knowledge_base | — | — | — | — | — | — | — | 2 |
use_outbound | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 1 |
view_ai_agents | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 3 |
view_audit_log | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 1 |
view_billing | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 8 |
view_documentos | — | — | — | — | — | — | — | 1 |
view_recados | — | — | — | — | — | — | — | 1 |
view_sip_trunks | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 1 |
view_telephony | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 10 |
| (sin capacidad exigida) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 33 |
16. Lo que el código no concede
Esta sección existe porque una integración que se construye contra una promesa comercial y choca con el código pierde semanas.
43 de las 134 operaciones no las alcanza de serie ningún plan comercial — tampoco Enterprise. Existen, están documentadas y funcionan; simplemente su capacidad no viene en ningún nivel de plan y hay que pedirla expresamente para la cuenta:
| Operación | Capacidad que exige |
|---|---|
DELETE /api/v1/campaigns/{campaign_id}/lists | manage_campaigns |
DELETE /api/v1/sequences/{sequence_id} | manage_campaigns |
DELETE /api/v1/team/invites/{invite_id} | manage_team |
GET /api/v1/campaigns | manage_campaigns |
GET /api/v1/campaigns/{campaign_id} | manage_campaigns |
GET /api/v1/campaigns/{campaign_id}/attempts | manage_campaigns |
GET /api/v1/campaigns/{campaign_id}/enrollments | manage_campaigns |
GET /api/v1/campaigns/{campaign_id}/lists | manage_campaigns |
GET /api/v1/documents | view_documentos |
GET /api/v1/email-templates | manage_campaigns |
GET /api/v1/email-templates/{template_id} | manage_campaigns |
GET /api/v1/email/connections | send_emails |
GET /api/v1/email/history | send_emails |
GET /api/v1/inbox/{mailbox_id}/conversations/{conversation_id} | send_emails |
GET /api/v1/inbox/{mailbox_id}/folders | send_emails |
GET /api/v1/inbox/{mailbox_id}/messages | send_emails |
GET /api/v1/inbox/{mailbox_id}/messages/{message_id} | send_emails |
GET /api/v1/inbox/{mailbox_id}/messages/{message_id}/attachments | send_emails |
GET /api/v1/inbox/{mailbox_id}/messages/{message_id}/attachments/{attachment_id} | send_emails |
GET /api/v1/inbox/mailboxes | send_emails |
GET /api/v1/knowledge-bases | use_knowledge_base |
GET /api/v1/members | manage_team |
GET /api/v1/recados | view_recados |
GET /api/v1/sequences | manage_campaigns |
GET /api/v1/sms/history | send_sms |
GET /api/v1/team/invites | manage_team |
GET /api/v1/team/mention-list | manage_team |
GET /api/v1/team/roles | manage_team |
GET /api/v1/team/seats | manage_team |
PATCH /api/v1/campaigns/{campaign_id} | manage_campaigns |
PATCH /api/v1/email-templates/{template_id} | manage_campaigns |
PATCH /api/v1/kb/collections/{collection_id} | manage_knowledge_bases |
PATCH /api/v1/sequences/{sequence_id} | manage_campaigns |
POST /api/v1/campaigns | manage_campaigns |
POST /api/v1/campaigns/{campaign_id}/dispatch | manage_campaigns |
POST /api/v1/campaigns/{campaign_id}/lists | manage_campaigns |
POST /api/v1/email-templates | manage_campaigns |
POST /api/v1/email/send | send_emails |
POST /api/v1/knowledge-bases/{kb_id}/search | use_knowledge_base |
POST /api/v1/sequences | manage_campaigns |
POST /api/v1/sequences/{sequence_id}/enroll | manage_campaigns |
POST /api/v1/sms/check | send_sms |
POST /api/v1/sms/send | send_sms |
Si tu integración depende de alguna, habla con tu distribuidor antes de escribir una línea. Es un permiso que se concede por cuenta y se resuelve en minutos, pero no se resuelve solo, y ninguna clave nueva lo arregla.
17. Dónde seguir
- Referencia interactiva — todos los esquemas y una consola para probar.
- Catálogo de operaciones — las 134, con su scope, su plan y su puerta.
- Historial de llamadas — la guía larga del dominio más usado.
- WhatsApp — enviar, recibir, plantillas y webhooks firmados.
- El documento OpenAPI en crudo:
/api/openapi.json.
¿Dudas o necesitas más límite? Escríbenos a info@amai.solutions.