Docs/Empezar

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 / PAYGNo. No hay pantalla de claves.
Starter (starter, pro, telephony-pilot)
Business
Agency · Partner · Enterprise

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

  1. Entra en el panel → API Keys (/app/api-keys).
  2. 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.
  3. Guárdala en una variable de entorno. Nunca en el repositorio, nunca en código que llegue al navegador.
bash
export AMAI_API_KEY="amai_..."
export AMAI_API_HOST="voice.amai.run"   # el tuyo, si contrataste con un revendedor

La 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)

bash
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:

code
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
  1. La clave. Existe, no está revocada, y llega en Authorization: Bearer amai_…. Un Bearer que no sea una clave válida da 401nunca cae de vuelta a la sesión del navegador, para que una clave muerta no parezca viva.
  2. 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.
  3. 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_scope y 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 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:

json
{ "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.

json
{ "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.

PuertaQué exigeOperaciones
SiempreNada72
EscrituraPUBLIC_API_WRITE_ENABLED41
GastoPUBLIC_API_WRITE_ENABLED y PUBLIC_API_MONEY_ENABLED y alta de la cuenta y Idempotency-Key12

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.

bash
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ónRespuestaQué significa
Reintento con la misma clave y el mismo cuerpo, ya terminadoEl resultado guardado, con Idempotency-Replayed: trueNo se ha vuelto a ejecutar.
Reintento mientras la primera sigue en vuelo409 idempotency_in_progressEspera y reintenta con la MISMA clave.
Misma clave con otro cuerpo409 idempotency_conflictUna clave por operación. Usa una nueva.
503 idempotency_unavailableEl registro no estaba disponibleNo se ha cobrado nada. Reintenta con la misma clave.
500 internal_error en una operación de gastoFallo por nuestro ladoReintenta 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:

json
{ "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.

bash
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))
done

Un 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 de page.next_cursor. Es opaco: no lo interpretes, y no lo reutilices entre listas distintas (te responde 400).
  • No hay total: contar un conjunto que se mueve da un número ya falso cuando lo lees.
json
{ "page": { "limit": 50, "has_more": true, "next_cursor": "eyJ2IjoxLC..." } }
bash
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')
done

La 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-01 o 2026-07-01T00:00:00Z. Nada relativo, nada local.
  • Los rangos son inclusivos por los dos extremos: end_date=2026-07-31 incluye 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_format antes de tocar la base de datos.

10. Límites de peticiones

LímitePresupuestoCómo se cuenta
Admisión600 / minPor IP, antes de mirar la clave. Solo existe para que una avalancha anónima no llegue a la base de datos.
Por clave60 / minPor 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:

json
{ "error": { "code": "invalid_field", "message": "..." } }

Es el sobre tipado, el de toda la superficie nueva.

json
{ "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/calendar
  • GET /api/v1/calendar/check
  • PUT /api/v1/calls/{call_id}/member
  • DELETE /api/v1/calls/{call_id}/member
  • POST /api/v1/contacts/identify
  • POST /api/v1/email/send
  • GET /api/v1/members
  • GET /api/v1/team/mention-list
  • GET /api/v1/whatsapp/messages

Una línea cubre las dos, para siempre:

js
const code = typeof body.error === "string" ? body.error : body.error?.code

Y 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.

HTTPCódigoSobreQué ha pasadoQué hacer
400agent_not_editabletipadoEl 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.
400batch_too_largetipadoEl lote enviado supera el máximo de elementos por petición.Trocea el lote. El máximo exacto viene en el mensaje del error.
400body_too_longtipadoEl 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.
400channel_unavailabletipadoLa 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.
400country_not_supportedtipadoEl 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.
400domain_not_settipadoSe 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.
400duplicate_daytipadoEl 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.
400event_not_emittedtipadoLos 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.
400fixed_linetipadoEl 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.
400invalid_bodytipadoEl 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.
400invalid_datelos dosUna fecha no es ISO-8601 válida.Usa 2026-07-01 o 2026-07-01T00:00:00Z. Nada de fechas relativas ni locales.
400invalid_default_countrytipadodefault_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.
400invalid_domaintipadocustom_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.
400invalid_fieldtipadoUn 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.
400INVALID_FILTERlegadoUn filtro de la query string no es válido.Revisa el nombre y el valor del filtro contra la referencia de la operación.
400invalid_id_formatlos dosUn 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.
400invalid_idempotency_keytipadoLa 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.
400invalid_jsonlos dosEl cuerpo no es JSON parseable.Revisa comas y comillas, y manda Content-Type: application/json.
400INVALID_JSONlegadoLo 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.
400invalid_labeltipadoLa etiqueta de llamada no cumple el formato admitido.Usa una clave corta sin espacios. El mensaje detalla el patrón.
400invalid_numbertipadoEl 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).
400invalid_phonetipadoEl número no está en E.164.Manda +34600111222: prefijo +, país y número, sin espacios ni guiones.
400invalid_rangelos dosEl 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.
400invalid_recipienttipadoLo mismo que INVALID_RECIPIENT, en el sobre tipado.Para SMS, un móvil en E.164 de un país habilitado en la cuenta.
400INVALID_RECIPIENTlegadoEl destinatario no es válido para el canal elegido.Para email, una dirección; para SMS y WhatsApp, un E.164.
400INVALID_REFERENCElegadocontact_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.
400invalid_requesttipadoLa 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.
400invalid_thresholdstipadoLos umbrales de aviso no forman una secuencia válida.Manda umbrales crecientes y dentro del rango que indica el mensaje.
400invalid_timelegadoUna hora del horario laboral no tiene formato HH:MM.Usa 24 h con dos dígitos: 09:00, 18:30.
400invalid_urltipadoEl 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.
400missing_contact_idstipadoLa operación necesita al menos un contacto y no llegó ninguno.Manda contact_ids con al menos un identificador.
400missing_fieldtipadoFalta un campo obligatorio del cuerpo.El mensaje nombra el campo. Añádelo.
400MISSING_FIELDSlegadoLo mismo que missing_field, en una operación del sobre legado.Idéntica corrección.
400missing_first_nametipadoUn contacto llega sin nombre de pila.first_name es obligatorio al crear un contacto.
400missing_idempotency_keytipadoUna 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.
400missing_nametipadoFalta el nombre del recurso que se está creando.Manda name.
400missing_phonetipadoFalta el teléfono en un recurso que lo exige.Manda el número en E.164.
400MISSING_QUERYlegadoFalta el parámetro de búsqueda obligatorio.Añade q (o el parámetro que indique la referencia de esa operación).
400missing_template_nametipadoUn envío de WhatsApp de tipo plantilla llegó sin nombre de plantilla.Manda template_name con una plantilla aprobada por Meta.
400missing_texttipadoUn envío de WhatsApp de texto llegó sin cuerpo.Manda text.
400missing_totipadoUn envío de WhatsApp llegó sin destinatario.Manda to en E.164.
400no_active_connectiontipadoEl 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.
400no_fieldstipadoUn 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.
400not_mobiletipadoNo 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.
400pricing_not_configuredtipadoEl 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.
400queue_label_not_allowedtipadoSe intentó escribir a mano una etiqueta reservada al sistema de colas.Usa otra clave de etiqueta. Las de cola las gestiona la plataforma.
400range_too_largelegadoEl rango de fechas pedido excede el máximo de esa operación.Trocéalo en ventanas más cortas y concaténalas tú.
400too_many_contactstipadoLa importación supera el máximo de contactos por petición.Divide el fichero. El máximo exacto viene en el mensaje.
400too_many_memberslegadoSe pidieron más miembros de los que la operación devuelve de una vez.Baja limit y pagina.
400too_many_recipientstipadoPOST /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.
401unauthenticatedlegadoLo mismo que unauthorized, en una operación del sobre legado.Idéntica corrección.
401unauthorizedlos dosFalta 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.
401UnauthorizedlegadoLo 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.
401UNAUTHORIZEDlegadoTercera capitalización del mismo fallo, en el sobre legado.Idéntica corrección. Normaliza a minúsculas antes de comparar.
402insufficient_balancetipadoLa 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ó.
403capability_requiredtipadoEl 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.
403domain_not_allowedtipadoEl 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.
403forbiddenlegadoDenegació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í.
403insufficient_permissiontipadoSolo 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).
403insufficient_scopelos dosLa 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í.
403INSUFFICIENT_SCOPElegadoLo mismo que insufficient_scope, en el sobre legado.Idéntica corrección.
403money_opt_in_requiredtipadoLa 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.
403permission_model_gaptipadoSolo 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.
403recipient_suppressedtipadoEl 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é.
403RECIPIENT_SUPPRESSEDlegadoEl 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.
403self_not_allowedtipadoLa 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.
404call_not_foundlegadoLa llamada no existe en tu tenant.Comprueba el call_id contra GET /api/v1/calls.
404member_not_foundlegadoEl miembro no existe en tu tenant.Comprueba el member_id contra GET /api/v1/members.
404No organization foundlegadoLa 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.
404no_organizationlos dosLa credencial no resuelve a ningún tenant activo.La cuenta puede estar suspendida. Contacta con tu distribuidor.
404not_foundtipadoEl 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.
409already_linkedlegadoEl recurso ya está asociado a lo que intentas asociarlo.No hace falta hacer nada: el estado que buscabas ya es el actual.
409domain_takentipadoEse 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.
409duplicate_contacttipadoYa existe un contacto con ese teléfono o email en tu tenant.Recupéralo y haz PATCH en vez de POST.
409idempotency_conflicttipadoEsa 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.
409idempotency_in_progresstipadoOtra 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.
409journey_stoppedtipadoEl 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.
409suppression_lift_deniedtipadoEl 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.
410target_not_activelegadoEl 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.
413attachment_too_largetipadoEl 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.
422outside_24h_windowtipadoIntentas 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.
429rate_limitedtipadoHas 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.
500Error internolegadoLa misma causa que internal_error, con el texto en prosa castellana.Idéntica conducta.
500Internal server errorlegadoLa misma causa que internal_error, con el texto en prosa inglesa.Idéntica conducta.
500internal_errorlos dosFallo 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.
502meta_errortipadoMeta (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.
502provider_errortipadoEl 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.
502upstream_errortipadoUn 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.
503connection_lookup_failedtipadoPOST /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.
503idempotency_unavailabletipadoEl 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.
503integration_not_configuredtipadoEl 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.
503recados_source_unavailabletipadoLa fuente de recados no responde.Reintenta más tarde. No es un error de tu petición.
503recipient_suppression_unverifiabletipadoNo 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.
503search_timeouttipadoLa búsqueda ha excedido el tiempo máximo.Acota la consulta (menos rango, más filtros) y reintenta.
503send_not_recordedtipadoNo 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.
503SUPPRESSION_UNVERIFIABLElegadoNo 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.

bash
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.

bash
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.

bash
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.

bash
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.

bash
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_ENABLED esté 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.

bash
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_ENABLED esté 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.

bash
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.

bash
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_ENABLED esté apagado.

Canal

Solo para cuentas de tipo agencia o partner: el árbol de subcuentas y sus estadísticas agregadas. 2 operaciones.

bash
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
  "https://$AMAI_API_HOST/api/v1/agency/stats"

Una cuenta cliente recibe 403 capability_required aquí, 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.

bash
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.

bash
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.

bash
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.

bash
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.

Email

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.

bash
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.

bash
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 exige PUBLIC_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.

bash
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.

bash
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.

bash
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.

bash
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.

bash
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.

bash
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_ENABLED esté 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.

bash
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.

bash
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_verified no se puede escribir. Se gana publicando el registro TXT que devuelve la lectura (_amai-verify.<dominio>amai-verify=<account_id>) y llamando a POST …/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.

bash
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_ENABLED esté apagado.

Secuencias

Secuencias de seguimiento automatizadas: crearlas y editarlas. 5 operaciones.

bash
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_ENABLED esté 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.

bash
curl -sS -H "Authorization: Bearer $AMAI_API_KEY" \
  "https://$AMAI_API_HOST/api/v1/numbers"

/api/v1/rates devuelve 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.

bash
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_ENABLED esté 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.

bash
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/webhooks solo dice si lo hay. Se enseña una única vez, en el POST que lo emite. Si lo pierdes, rotate_secret emite otro — y las firmas anteriores dejan de validar.

WhatsApp

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.

bash
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».

ScopeQué autorizaCapacidad de tenantPlanes que la traen
agency:readLeer datos de canal y asignaciones (solo agency/partner)create_subaccountsAgency, Partner, Enterprise
agency:writeCrear y gestionar subcuentas (solo agency/partner)create_subaccountsAgency, Partner, Enterprise
agents:readListar y ver agentes de vozview_ai_agentsTodos
agents:writeEditar el contenido de un agente (prompt, saludo, voz)edit_ai_agentsTodos
alerts:readLeer alertas y avisos del sistema— (ninguna)Todos
alerts:writeConfigurar, descartar o silenciar alertas— (ninguna)Todos
analytics:readLeer el dashboard y las estadísticas de llamadas— (ninguna)Todos
announcements:readLeer los anuncios internos publicados por el tenant— (ninguna)Todos
api-keys:writeRotar la propia clave de APImanage_api_keysStarter, Business, Agency, Partner, Enterprise
audit:readLeer el registro de actividad del tenantview_audit_logTodos
billing:readConsultar saldo, suscripción, consumo y facturasview_billingTodos
billing:writeContratar plan o add-on y recargar saldo (mueve dinero)manage_billingTodos
branding:readLeer la marca blanca y las verjas de empaquetado del árbolmanage_descendant_brandingAgency, Partner, Enterprise
branding:writeEditar la marca blanca y las verjas de empaquetado del árbolmanage_descendant_brandingAgency, Partner, Enterprise
calendar:readLeer el calendario laboral y su disponibilidad— (ninguna)Todos
calendar:writeEditar el calendario laboral y sus excepciones— (ninguna)Todos
calls:dialLanzar una llamada saliente (gasta saldo)use_outboundTodos
calls:readLeer el historial de llamadas— (ninguna)Todos
calls:writeAnotar llamadas (miembro asignado, etiquetas, comentarios)— (ninguna)Todos
campaigns:dispatchDespachar una campaña (gasta saldo)manage_campaignsNinguno de serie
campaigns:readListar campañas, secuencias y plantillasmanage_campaignsNinguno de serie
campaigns:writeCrear y editar campañas, secuencias y plantillasmanage_campaignsNinguno de serie
contacts:readListar y buscar contactos— (ninguna)Todos
contacts:writeCrear y editar contactos— (ninguna)Todos
documents:readListar el archivo documental del tenantview_documentosNinguno de serie
email-configs:readVer las identidades de envío de email conectadassend_emailsNinguno de serie
email:readLeer el histórico de emailsend_emailsNinguno de serie
email:sendEnviar emailsend_emailsNinguno de serie
inbox:readLeer los buzones compartidos del tenant (bandeja de entrada)send_emailsNinguno de serie
invites:revokeRevocar una invitación de equipo pendientemanage_teamNinguno de serie
kb:readListar y buscar en la base de conocimientouse_knowledge_baseNinguno de serie
kb:writeSubir y gestionar documentos de la base de conocimientomanage_knowledge_basesNinguno de serie
members:readVer los miembros de la cuentamanage_teamNinguno de serie
numbers:readListar los números del tenantview_telephonyTodos
numbers:writeConfigurar o liberar un númeromanage_didsTodos
rates:readConsultar las tarifas propias del tenantmanage_ratesStarter, Business, Agency, Partner, Enterprise
recados:readLeer la bandeja de recadosview_recadosNinguno de serie
sip-trunks:readVer los troncales SIP del tenantview_sip_trunksTodos
sip-trunks:writeCrear y gestionar troncales SIPcreate_sip_trunksPAYG, Starter, Business, Agency, Partner, Enterprise
sms:readLeer el histórico de SMSsend_smsNinguno de serie
sms:sendEnviar SMSsend_smsNinguno de serie
subaccounts:readVer las subcuentas del árbol (solo agency/partner)create_subaccountsAgency, Partner, Enterprise
team:readVer el equipo del tenantmanage_teamNinguno de serie
webhooks:readVer la suscripción de webhooks y el registro de entregasview_telephonyTodos
webhooks:writeRegistrar, modificar y eliminar el destino de webhooksview_telephonyTodos
whatsapp:readLeer conversaciones y plantillas de WhatsAppconnect_whatsappTodos
whatsapp:sendEnviar mensajes de WhatsAppmanage_whatsappTodos

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.

CapacidadFreePAYGStarterBusinessAgencyPartnerEnterpriseOperaciones
connect_whatsapp6
create_sip_trunks3
create_subaccounts5
edit_ai_agents1
manage_api_keys1
manage_billing5
manage_campaigns19
manage_descendant_branding5
manage_dids1
manage_knowledge_bases1
manage_rates2
manage_team6
manage_whatsapp5
send_emails10
send_sms3
use_knowledge_base2
use_outbound1
view_ai_agents3
view_audit_log1
view_billing8
view_documentos1
view_recados1
view_sip_trunks1
view_telephony10
(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ónCapacidad que exige
DELETE /api/v1/campaigns/{campaign_id}/listsmanage_campaigns
DELETE /api/v1/sequences/{sequence_id}manage_campaigns
DELETE /api/v1/team/invites/{invite_id}manage_team
GET /api/v1/campaignsmanage_campaigns
GET /api/v1/campaigns/{campaign_id}manage_campaigns
GET /api/v1/campaigns/{campaign_id}/attemptsmanage_campaigns
GET /api/v1/campaigns/{campaign_id}/enrollmentsmanage_campaigns
GET /api/v1/campaigns/{campaign_id}/listsmanage_campaigns
GET /api/v1/documentsview_documentos
GET /api/v1/email-templatesmanage_campaigns
GET /api/v1/email-templates/{template_id}manage_campaigns
GET /api/v1/email/connectionssend_emails
GET /api/v1/email/historysend_emails
GET /api/v1/inbox/{mailbox_id}/conversations/{conversation_id}send_emails
GET /api/v1/inbox/{mailbox_id}/folderssend_emails
GET /api/v1/inbox/{mailbox_id}/messagessend_emails
GET /api/v1/inbox/{mailbox_id}/messages/{message_id}send_emails
GET /api/v1/inbox/{mailbox_id}/messages/{message_id}/attachmentssend_emails
GET /api/v1/inbox/{mailbox_id}/messages/{message_id}/attachments/{attachment_id}send_emails
GET /api/v1/inbox/mailboxessend_emails
GET /api/v1/knowledge-basesuse_knowledge_base
GET /api/v1/membersmanage_team
GET /api/v1/recadosview_recados
GET /api/v1/sequencesmanage_campaigns
GET /api/v1/sms/historysend_sms
GET /api/v1/team/invitesmanage_team
GET /api/v1/team/mention-listmanage_team
GET /api/v1/team/rolesmanage_team
GET /api/v1/team/seatsmanage_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/campaignsmanage_campaigns
POST /api/v1/campaigns/{campaign_id}/dispatchmanage_campaigns
POST /api/v1/campaigns/{campaign_id}/listsmanage_campaigns
POST /api/v1/email-templatesmanage_campaigns
POST /api/v1/email/sendsend_emails
POST /api/v1/knowledge-bases/{kb_id}/searchuse_knowledge_base
POST /api/v1/sequencesmanage_campaigns
POST /api/v1/sequences/{sequence_id}/enrollmanage_campaigns
POST /api/v1/sms/checksend_sms
POST /api/v1/sms/sendsend_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

¿Dudas o necesitas más límite? Escríbenos a info@amai.solutions.