Agil API
Conecta tu sistema —tu CRM, tu tienda en línea, tu software— con WorkAgil. Manda SMS, consulta cómo terminó cada uno y recibe un aviso en tu sistema cuando se entrega o falla, sin entrar al panel.
https://api.workagil.com/v1- Una llave por cada sistema que se conecta. La creas en tu cuenta WorkAgil.
- JSON de ida y de vuelta, y los mismos errores en todos los pedidos.
- Llaves de prueba que responden igual que las reales, pero no mandan nada ni gastan saldo.
- Webhooks firmados para que tu sistema se entere solo de lo que pasa.
Primeros pasos
En cinco minutos mandas tu primer SMS de prueba.
1. Crea una llave de prueba
Entra a secure.workagil.com → Llaves de API y toca Crear llave. Elige Prueba, el permiso SMS y, para empezar, IP dinámica. La llave se muestra una sola vez: cópiala y guárdala en un lugar seguro. Empieza por wa_test_.
Para usar la API tu empresa necesita Agil API. Si no lo tiene, solicítalo en Mis servicios: te escribimos para activarlo. Puedes crear la llave mientras tanto.
2. Pruébala
Pide /cuenta. Si la llave está bien, responde con tu empresa y tus servicios.
curl https://api.workagil.com/v1/cuenta \
-H "Authorization: Bearer wa_test_TU_LLAVE"
// Node 18 o más nuevo (fetch viene incluido)
const respuesta = await fetch('https://api.workagil.com/v1/cuenta', {
headers: { Authorization: `Bearer ${process.env.WORKAGIL_LLAVE}` },
});
console.log(respuesta.status, await respuesta.json());
import os, requests
respuesta = requests.get(
"https://api.workagil.com/v1/cuenta",
headers={"Authorization": f"Bearer {os.environ['WORKAGIL_LLAVE']}"},
timeout=20,
)
print(respuesta.status_code, respuesta.json())
<?php
$ch = curl_init('https://api.workagil.com/v1/cuenta');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WORKAGIL_LLAVE')],
CURLOPT_RETURNTRANSFER => true,
]);
$cuerpo = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), ' ', $cuerpo;
3. Manda tu primer SMS
Con una llave de prueba el SMS no sale ni se cobra, pero la respuesta, la consulta y el webhook son iguales a los reales.
curl https://api.workagil.com/v1/sms \
-H "Authorization: Bearer wa_test_TU_LLAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-98231" \
-d '{"para": "+573001234567", "texto": "Hola Laura, tu pedido 98231 sale hoy.", "referencia": "pedido-98231"}'
const respuesta = await fetch('https://api.workagil.com/v1/sms', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WORKAGIL_LLAVE}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'pedido-98231',
},
body: JSON.stringify({
para: '+573001234567',
texto: 'Hola Laura, tu pedido 98231 sale hoy.',
referencia: 'pedido-98231',
}),
});
const sms = await respuesta.json();
console.log(respuesta.status, sms.id, sms.estado);
import os, requests
respuesta = requests.post(
"https://api.workagil.com/v1/sms",
headers={
"Authorization": f"Bearer {os.environ['WORKAGIL_LLAVE']}",
"Idempotency-Key": "pedido-98231",
},
json={
"para": "+573001234567",
"texto": "Hola Laura, tu pedido 98231 sale hoy.",
"referencia": "pedido-98231",
},
timeout=30,
)
sms = respuesta.json()
print(respuesta.status_code, sms["id"], sms["estado"])
<?php
$ch = curl_init('https://api.workagil.com/v1/sms');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WORKAGIL_LLAVE'),
'Content-Type: application/json',
'Idempotency-Key: pedido-98231',
],
CURLOPT_POSTFIELDS => json_encode([
'para' => '+573001234567',
'texto' => 'Hola Laura, tu pedido 98231 sale hoy.',
'referencia' => 'pedido-98231',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$sms = json_decode(curl_exec($ch), true);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), ' ', $sms['id'], ' ', $sms['estado'];
Responde 201 con el SMS. Guarda su id para consultarlo después.
4. Pasa a producción
Cuando todo funcione, crea una llave de Producción (wa_live_). Necesita Agil API activo en tu empresa y la verificación en dos pasos en tu cuenta. Cambia la llave en tu sistema: lo demás es igual.
Llaves y seguridad
Cada pedido lleva la llave en la cabecera:
Authorization: Bearer wa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| Tipo | Empieza por | Qué hace |
|---|---|---|
| Prueba | wa_test_ | Responde igual que la real, pero no manda SMS ni gasta saldo. Ve solo lo que mandaste con llaves de prueba. |
| Producción | wa_live_ | Manda de verdad y descuenta de la bolsa de tu empresa. Necesita Agil API activo y la verificación en dos pasos. |
Lo que configuras en cada llave
- Estado: Habilitada o Deshabilitada. Deshabilitada, responde
401 llave_deshabilitadaal momento; la vuelves a habilitar cuando quieras. - Desde qué IP: IPs fijas (una o varias, también rangos como
190.24.10.0/24; hasta 20) o IP dinámica, que acepta cualquier IP. Usa IPs fijas siempre que tu servidor tenga una IP fija: si la llave se filtra, no sirve desde otro lado. - Permisos: qué servicios puede usar (
smspara todo lo de esta página). Solo aparecen los que tiene tu empresa.
En la misma pantalla ves cuándo y desde qué IP se usó cada llave por última vez, y la lista de sus últimos pedidos con el resultado.
Nunca pongas la llave en una página web o una app: cualquiera la podría ver. Úsala solo desde tu servidor. Si crees que se filtró, deshabilítala y crea otra.
Reglas comunes
JSON y fechas
Los pedidos con cuerpo llevan Content-Type: application/json. Las fechas van en ISO 8601 con zona, por ejemplo 2026-10-28T16:30:39.857Z.
Teléfonos
En formato internacional: +573001234567 (también se acepta sin el +). Para Colombia puedes mandar el número sin el 57 si agregas "pais": "CO". Solo se aceptan los destinos habilitados para tu empresa (por defecto, celulares y fijos de Colombia).
Partes de un SMS
Un SMS largo viaja partido, y cada parte se cobra. Sin tildes ni emojis caben 160 caracteres en una parte (153 por parte si es más largo). Con á, í, ó, ú o emojis, el mensaje pasa a 70 por parte (67 si es más largo). Cada SMS dice en partes cuántas usó. Máximo 1.000 caracteres.
Pedidos repetidos: Idempotency-Key
Si tu sistema reintenta un envío (se cortó la conexión, venció el tiempo), manda la misma cabecera Idempotency-Key con un valor tuyo, por ejemplo el número de pedido. Con la misma clave el SMS no sale dos veces: la respuesta es el SMS de la primera vez, con 200 en vez de 201. Hasta 200 caracteres visibles, sin espacios.
Límite de pedidos
60 pedidos por minuto por llave. Al pasarte, la respuesta es 429 demasiados_pedidos con la cabecera Retry-After: los segundos que hay que esperar. Las cabeceras RateLimit y RateLimit-Policy dicen cuánto te queda. Un lote de hasta 1.000 SMS cuenta como un solo pedido.
Listas
Las listas vienen en páginas: ?limite=50 (hasta 100). Si hay más, la respuesta trae siguiente; pide la próxima página con ?despues= y ese valor.
Errores
Todos los errores tienen la misma forma. codigo es para tu programa; mensaje, para la persona que lo lee. Algunos traen más datos, como campo.
{
"error": {
"codigo": "telefono_invalido",
"mensaje": "El número no es válido. Usa el formato internacional (+573001234567), o manda pais: \"CO\".",
"campo": "para"
}
}| HTTP | codigo | Cuándo |
|---|---|---|
| 400 | datos_invalidos | Falta un dato o tiene mal formato. campo dice cuál. |
| 400 | telefono_invalido | El número no sirve, o no es de un destino permitido para tu empresa. |
| 401 | llave_invalida | Sin llave, mal escrita o borrada. |
| 401 | llave_deshabilitada | La llave existe pero está deshabilitada. |
| 402 | saldo_insuficiente | No hay saldo de SMS para el envío. necesita dice cuántas partes hacían falta. No se mandó nada. |
| 403 | ip_no_permitida | La llave tiene IPs fijas y el pedido vino de otra. |
| 403 | sin_permiso | La llave no tiene el permiso de ese servicio. |
| 403 | servicio_no_incluido | Tu empresa no tiene ese servicio (o no tiene Agil API). |
| 403 | sin_agil_api | Llave de producción sin Agil API activo en tu empresa. |
| 403 | sin_empresa | Tu cuenta de WorkAgil no administra ninguna empresa en el panel. |
| 403 | cuenta_suspendida, cuenta_bloqueada | La cuenta o la empresa no pueden usar el servicio ahora (por ejemplo, un pago pendiente). |
| 404 | no_existe | El SMS o el pedido no existe, o no es de tu empresa. |
| 413 | demasiado_grande | El cuerpo pasa de 1 MB. |
| 422 | en_lista_no_llamar | El número está en la lista de no llamar de tu empresa. |
| 429 | demasiados_pedidos | Pasaste el límite por minuto. Espera lo que dice Retry-After. |
| 500 | error_interno | Algo falló de nuestro lado. Reintenta (con la misma Idempotency-Key). |
| 503 | servicio_no_disponible | El servicio o el proveedor de SMS no respondió. No se cobró nada; reintenta en unos minutos. |
Con qué cuenta quedó tu llave
/v1/cuentaCualquier llave válidaEl primer pedido para probar una llave: dice a qué empresa pertenece y qué servicios tiene.
{
"llave": { "nombre": "CRM Bogotá", "modo": "prueba", "permisos": ["sms"] },
"empresa": { "id": 35, "nombre": "Muebles Laura", "plan": "Solo API" },
"servicios": ["agil-blast", "agil-api"]
}Mandar un SMS
/v1/smsPermiso sms| Campo | Tipo | Descripción |
|---|---|---|
para | texto | Obligatorio. El número, en formato internacional. |
texto | texto | Obligatorio. El mensaje, hasta 1.000 caracteres. |
referencia | texto | Opcional, hasta 100 caracteres. Tuya: vuelve igual en la consulta y en el webhook, para que tu sistema sepa a qué pedido pertenece. |
pais | texto | Opcional. "CO" permite mandar el número sin el 57. |
Cabecera opcional: Idempotency-Key.
El saldo se descuenta antes de mandar, por cada parte. Si el proveedor no recibe el envío, se devuelve. Responde 201 con el objeto SMS:
{
"id": "sms_21",
"para": "+573001234567",
"texto": "Hola Laura, tu pedido 98231 sale hoy.",
"estado": "enviado",
"en_operador": false,
"partes": 1,
"referencia": "pedido-98231",
"origen": "api",
"campana": null,
"prueba": false,
"creado": "2026-10-28T16:30:39.857Z",
"entregado": null,
"motivo": null
}Errores típicos: telefono_invalido, datos_invalidos, en_lista_no_llamar, saldo_insuficiente. Ver Errores.
Mandar SMS en lote
/v1/sms/lotePermiso sms · hasta 1.000El mismo texto para todos, o uno por número. Los números que no se pueden mandar (lista de no llamar, destino no permitido) se rechazan uno por uno, sin frenar a los demás y sin cobrarse. Si un número tiene mal formato, el lote completo responde 400 y campo dice cuál (por ejemplo mensajes[3].para).
| Campo | Tipo | Descripción |
|---|---|---|
mensajes | lista | Obligatorio. De 1 a 1.000 elementos { para, texto?, referencia?, pais? }. |
texto | texto | Opcional. El texto para los mensajes que no traen el suyo. |
pais | texto | Opcional. "CO" para todo el lote. |
curl https://api.workagil.com/v1/sms/lote \
-H "Authorization: Bearer wa_test_TU_LLAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: promo-octubre" \
-d '{
"texto": "Promo de octubre: 20% en salas. Responde SALIR para no recibir mas.",
"pais": "CO",
"mensajes": [
{ "para": "3001112233", "referencia": "cliente-77" },
{ "para": "3004445566", "referencia": "cliente-78" },
{ "para": "3007778899", "texto": "Laura, tu sala ya llego a la tienda." }
]
}'
const respuesta = await fetch('https://api.workagil.com/v1/sms/lote', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WORKAGIL_LLAVE}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'promo-octubre',
},
body: JSON.stringify({
texto: 'Promo de octubre: 20% en salas. Responde SALIR para no recibir mas.',
pais: 'CO',
mensajes: clientes.map((c) => ({ para: c.celular, referencia: `cliente-${c.id}` })),
}),
});
const lote = await respuesta.json();
console.log(`${lote.aceptados} aceptados, ${lote.rechazados} rechazados, ${lote.partes} partes`);
for (const m of lote.mensajes.filter((m) => m.error)) console.log(m.para, m.error.codigo);
respuesta = requests.post(
"https://api.workagil.com/v1/sms/lote",
headers={
"Authorization": f"Bearer {os.environ['WORKAGIL_LLAVE']}",
"Idempotency-Key": "promo-octubre",
},
json={
"texto": "Promo de octubre: 20% en salas. Responde SALIR para no recibir mas.",
"pais": "CO",
"mensajes": [{"para": c["celular"], "referencia": f"cliente-{c['id']}"} for c in clientes],
},
timeout=60,
)
lote = respuesta.json()
print(lote["aceptados"], "aceptados,", lote["rechazados"], "rechazados")
for m in lote["mensajes"]:
if "error" in m:
print(m["para"], m["error"]["codigo"])
<?php
$mensajes = array_map(fn ($c) => ['para' => $c['celular'], 'referencia' => 'cliente-' . $c['id']], $clientes);
$ch = curl_init('https://api.workagil.com/v1/sms/lote');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WORKAGIL_LLAVE'),
'Content-Type: application/json',
'Idempotency-Key: promo-octubre',
],
CURLOPT_POSTFIELDS => json_encode([
'texto' => 'Promo de octubre: 20% en salas. Responde SALIR para no recibir mas.',
'pais' => 'CO',
'mensajes' => $mensajes,
]),
CURLOPT_RETURNTRANSFER => true,
]);
$lote = json_decode(curl_exec($ch), true);
echo "{$lote['aceptados']} aceptados, {$lote['rechazados']} rechazados\n";
{
"total": 3,
"aceptados": 2,
"rechazados": 1,
"partes": 2,
"mensajes": [
{ "id": "sms_31", "para": "+573001112233", "estado": "enviado", "referencia": "cliente-77", "partes": 1, "...": "..." },
{ "para": "+573004445566", "referencia": "cliente-78",
"error": { "codigo": "en_lista_no_llamar", "mensaje": "El número está en la lista de no llamar de la empresa." } },
{ "id": "sms_32", "para": "+573007778899", "estado": "enviado", "partes": 1, "...": "..." }
]
}Los mensajes vuelven en el mismo orden en que los mandaste. El saldo se cobra todo o nada: si no alcanza para los aceptados, el lote responde 402 saldo_insuficiente y no sale ninguno.
Consultar un SMS
/v1/sms/{id}Permiso smsCómo va un SMS. El estado cambia mientras el operador móvil intenta entregarlo (hasta 48 horas). Si prefieres no preguntar, usa los webhooks.
curl https://api.workagil.com/v1/sms/sms_21 \
-H "Authorization: Bearer wa_test_TU_LLAVE"
const respuesta = await fetch('https://api.workagil.com/v1/sms/sms_21', {
headers: { Authorization: `Bearer ${process.env.WORKAGIL_LLAVE}` },
});
const sms = await respuesta.json();
console.log(sms.estado, sms.en_operador, sms.motivo);
sms = requests.get(
"https://api.workagil.com/v1/sms/sms_21",
headers={"Authorization": f"Bearer {os.environ['WORKAGIL_LLAVE']}"},
timeout=20,
).json()
print(sms["estado"], sms["en_operador"], sms["motivo"])
<?php
$ch = curl_init('https://api.workagil.com/v1/sms/sms_21');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('WORKAGIL_LLAVE')],
CURLOPT_RETURNTRANSFER => true,
]);
$sms = json_decode(curl_exec($ch), true);
echo $sms['estado'];
Una llave de prueba solo ve los SMS de prueba, y una de producción solo los reales.
Listar SMS
/v1/smsPermiso smsLos SMS de tu empresa, del más nuevo al más viejo: los que mandó tu sistema y los de tus campañas del panel.
| Parámetro | Descripción |
|---|---|
limite | Cuántos por página, de 1 a 100. Por defecto 50. |
despues | El valor de siguiente de la página anterior. |
desde, hasta | Fechas ISO 8601, por ejemplo 2026-10-01. |
estado | enviado, entregado o fallido. |
origen | api (los de tu sistema) o campana (los del panel). |
referencia | Los que mandaste con esa referencia. |
curl "https://api.workagil.com/v1/sms?limite=50&estado=fallido&desde=2026-10-01" \
-H "Authorization: Bearer wa_live_TU_LLAVE"
{
"datos": [ { "id": "sms_120", "estado": "fallido", "motivo": "El operador móvil no lo entregó.", "...": "..." } ],
"siguiente": "sms_71"
}Si siguiente no es null, hay más: pide /v1/sms?despues=sms_71 con los mismos filtros.
Saldo de SMS
/v1/saldoPermiso smsCuántas partes de SMS quedan en la bolsa de tu empresa. Con una llave de prueba también ves el saldo real (las pruebas no lo gastan).
{ "sms": 4200 }El objeto SMS
| Campo | Descripción |
|---|---|
id | Identificador, como sms_21. |
para | El número, con +. |
texto | El mensaje que salió. |
estado |
enviado: el proveedor lo aceptó y lo está entregando. entregado: se confirmó la entrega en el celular. fallido: el proveedor lo rechazó o el operador no lo entregó; motivo dice por qué. |
en_operador | true si ya llegó a la red del operador móvil (Claro, Movistar, Tigo…). Con SMS normales es lo último que se sabe: los operadores casi nunca confirman la entrega al celular. |
partes | Cuántas partes ocupó (y se cobraron). |
referencia | La que mandaste, o null. |
origen | api o campana. |
campana | Si salió de una campaña del panel: { id, nombre }. |
prueba | true si se mandó con una llave de prueba (no salió ni se cobró). |
creado | Cuándo se mandó. |
entregado | Cuándo se confirmó la entrega, si se confirmó. |
motivo | Por qué falló, si falló. |
Webhooks
En vez de preguntar cada rato, deja que WorkAgil le avise a tu sistema. Se configuran en el panel: app.workagil.com → API → Webhooks. Ahí pones la dirección de tu sistema (con https://), eliges los eventos y generas el secreto con el que se firma cada aviso. También puedes mandar un aviso de prueba y ver el registro de lo que se mandó y qué respondió tu sistema.
Eventos de SMS
| Evento | Cuándo |
|---|---|
sms.entregado | Se confirmó la entrega en el celular. Con llaves de prueba sale enseguida. |
sms.fallido | El proveedor lo rechazó al enviar, o el operador no lo entregó. |
Salen para los SMS de tu sistema y para los de tus campañas; los distingues por origen. Además hay eventos de llamadas y cobranza: llamada.terminada, resultado.agente, acuerdo.pago, pago.registrado, promesa.cumplida y promesa.incumplida.
Lo que llega
Un POST con este cuerpo, y estas cabeceras:
POST https://tu-sistema.com/webhooks/workagil
Content-Type: application/json
X-Webhook-Evento: sms.fallido
X-Webhook-Id: 3f1c5a9e-0b7d-4f2e-9a61-2d8e4c7b1a05
X-Webhook-Firma: t=1793205039,v1=5b1e0c…
{
"id": "3f1c5a9e-0b7d-4f2e-9a61-2d8e4c7b1a05",
"evento": "sms.fallido",
"ocurrido_at": "2026-10-28T16:34:00.000Z",
"datos": {
"sms": {
"id": "sms_21",
"para": "+573001234567",
"estado": "fallido",
"motivo": "El operador móvil no lo entregó.",
"referencia": "pedido-98231",
"origen": "api",
"prueba": false,
"...": "el resto del objeto SMS"
},
"campana": null
}
}- Responde rápido con un 2xx (en menos de 10 segundos). Si tu sistema necesita hacer algo largo, guarda el aviso y procésalo después.
- Si no responde 2xx, se reintenta a los 1, 5, 30, 120 y 720 minutos.
- Un reintento trae el mismo
id(también enX-Webhook-Id): úsalo para no procesar dos veces el mismo aviso.
Comprobar la firma
Cualquiera que conozca la dirección de tu sistema le puede mandar un POST. La firma demuestra que el aviso salió de WorkAgil y que nadie lo cambió en el camino.
X-Webhook-Firma trae t (la hora del envío, en segundos) y v1: el HMAC-SHA256, en hexadecimal, de t + "." + cuerpo con tu secreto (whsec_…). Para comprobarla:
- Lee el cuerpo tal como llegó, antes de convertirlo de JSON: un espacio de diferencia cambia la firma.
- Calcula el HMAC-SHA256 de
t.cuerpocon tu secreto y compáralo conv1de forma segura (en tiempo constante). - Rechaza los avisos con un
tde hace más de 5 minutos: así nadie puede reenviar uno viejo.
// Node con Express
const crypto = require('crypto');
const express = require('express');
function firmaValida(cuerpo, cabecera, secreto) {
const partes = Object.fromEntries(cabecera.split(',').map((p) => p.split('=')));
const t = Number(partes.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const esperada = crypto.createHmac('sha256', secreto).update(`${t}.${cuerpo}`).digest();
const recibida = Buffer.from(partes.v1 || '', 'hex');
return recibida.length === esperada.length && crypto.timingSafeEqual(recibida, esperada);
}
const app = express();
// express.raw: el cuerpo llega tal cual, sin convertir.
app.post('/webhooks/workagil', express.raw({ type: 'application/json' }), (req, res) => {
const cuerpo = req.body.toString('utf8');
if (!firmaValida(cuerpo, req.get('X-Webhook-Firma') || '', process.env.WORKAGIL_WEBHOOK_SECRETO)) {
return res.sendStatus(401);
}
const aviso = JSON.parse(cuerpo);
if (aviso.evento === 'sms.fallido') {
console.log('No llegó', aviso.datos.sms.referencia, aviso.datos.sms.motivo);
}
res.sendStatus(200);
});
app.listen(3000);
# Python con Flask
import hashlib, hmac, json, os, time
from flask import Flask, request
app = Flask(__name__)
SECRETO = os.environ["WORKAGIL_WEBHOOK_SECRETO"].encode()
def firma_valida(cuerpo: bytes, cabecera: str) -> bool:
partes = dict(p.split("=", 1) for p in cabecera.split(",") if "=" in p)
try:
t = int(partes.get("t", ""))
except ValueError:
return False
if abs(time.time() - t) > 300:
return False
esperada = hmac.new(SECRETO, f"{t}.".encode() + cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, partes.get("v1", ""))
@app.post("/webhooks/workagil")
def webhook():
cuerpo = request.get_data() # tal como llegó
if not firma_valida(cuerpo, request.headers.get("X-Webhook-Firma", "")):
return "", 401
aviso = json.loads(cuerpo)
if aviso["evento"] == "sms.fallido":
print("No llegó", aviso["datos"]["sms"]["referencia"])
return "", 200
<?php
$secreto = getenv('WORKAGIL_WEBHOOK_SECRETO');
$cuerpo = file_get_contents('php://input'); // tal como llegó
$cabecera = $_SERVER['HTTP_X_WEBHOOK_FIRMA'] ?? '';
$partes = [];
foreach (explode(',', $cabecera) as $p) {
[$clave, $valor] = array_pad(explode('=', $p, 2), 2, '');
$partes[$clave] = $valor;
}
$t = (int) ($partes['t'] ?? 0);
$esperada = hash_hmac('sha256', $t . '.' . $cuerpo, $secreto);
if ($t === 0 || abs(time() - $t) > 300 || !hash_equals($esperada, $partes['v1'] ?? '')) {
http_response_code(401);
exit;
}
$aviso = json_decode($cuerpo, true);
if ($aviso['evento'] === 'sms.fallido') {
error_log('No llegó ' . $aviso['datos']['sms']['referencia']);
}
http_response_code(200);
Lo que viene
Agil API crece por servicio. Lo que ya está planeado:
- Mensajes de voz en lote: llamar a una lista y reproducir un audio o un texto.
- Campañas (Agil Call): agregar contactos a una campaña, iniciarla o pausarla, y descargar llamadas y grabaciones; una llamada al momento con el agente de voz IA.
- Central (Agil Central): extensiones, registro de llamadas, clic para llamar desde tu CRM y buzón de voz.
- Cobranza (Superinteligencia): crear gestiones con la cartera en JSON, informar pagos y consultar promesas.
En el panel, API → APIs disponibles muestra qué ya está listo para tu empresa.
WorkAgil · Agil API v1 · ¿Dudas? Escríbenos por WhatsApp al +57 316 135 0011.