- Cree una cuenta y luego una clave sandbox en Panel → Claves API, y expórtela: export ENVOISMS_API_KEY=env_test_…
- Pegue uno de los ejemplos de al lado: funciona tal cual con esa clave, sin enviar ni cobrar nada.
- Registre un webhook para recibir los estados de entrega y las respuestas.
- Recargue, cree una clave live (smr_…) y sustitúyala por la clave sandbox en su configuración.
1. Enviar un SMS
Una sola llamada. Con una clave sandbox (env_test_…) el envío se simula: nada sale de la plataforma, no se cobra nada y la respuesta indica el precio que pagaría una clave live (simulated_cost).
RespuestaHTTP/1.1 202 Accepted
{
"id": "msg_9f2c4e1a7b3d",
"to": "+212612345678",
"channel": "sms",
"cascade": false,
"status": "queued",
"cost": { "eur": 0, "mad": 0 },
"sandbox": true,
"simulated_cost": { "eur": 0.0704, "mad": 0.792 },
"segments": 1,
"created_at": "2026-10-22T10:30:00.000Z"
}curl https://api.envoisms.ma/v1/messages \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"to": "+212612345678",
"message": "Your order #1042 has shipped."
}'2. Enviar una plantilla de WhatsApp
WhatsApp envía desde su propio número de WhatsApp Business, conectado una vez en el panel (si no, 403 WHATSAPP_NOT_CONNECTED). Fuera de la ventana de 24 h solo una plantilla aprobada puede abrir la conversación; sus variables {{1}}, {{2}}… van en metadata.variables, en orden.
RespuestaHTTP/1.1 202 Accepted
{
"id": "msg_4b81d0c2e9f7",
"to": "+212612345678",
"channel": "whatsapp",
"cascade": false,
"status": "queued",
"cost": { "eur": 0.05778, "mad": 0.65 },
"segments": 1,
"created_at": "2026-10-22T10:31:00.000Z"
}curl https://api.envoisms.ma/v1/messages \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"channel": "whatsapp",
"template": { "name": "order_shipped", "language": "fr" },
"metadata": { "variables": ["Karim", "#1042"] }
}'3. Verificar un número (OTP)
EnvoiSMS genera el código, lo entrega, cuenta los intentos y lo hace caducar (10 min por defecto). Usted solo guarda session_id. Con una clave sandbox la respuesta incluye sandbox_code para probar /verify/check sin teléfono.
RespuestaPOST /v1/verify/send → 202 Accepted
{
"session_id": "vrf_2c7e9a14b0d3",
"message_id": "msg_e1a07c93b5f2",
"to": "+212612345678",
"channel": "sms",
"expires_at": "2026-10-22T10:42:00.000Z",
"resend_after_seconds": 30,
"fallback_channels": [],
"status": "sent",
"sandbox": true,
"simulated": true,
"sandbox_code": "483921"
}
POST /v1/verify/check → 200 OK
{ "session_id": "vrf_2c7e9a14b0d3", "verified": true, "verified_at": "2026-10-22T10:33:12.000Z" }# 1. Send the code
curl https://api.envoisms.ma/v1/verify/send \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "+212612345678", "brand": "MyApp" }'
# 2. Check what the user typed
curl https://api.envoisms.ma/v1/verify/check \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "session_id": "vrf_…", "code": "483921" }'Autenticación
Autenticación#
Cada petición /v1 lleva una clave API en la cabecera Authorization. Cree, limite y revoque sus claves en Panel → Claves API; una clave se muestra una sola vez, al crearla. Úsela solo en el servidor.
smr_… envía de verdad y descuenta del saldo; env_test_… lo simula todo, gratis. Misma URL para ambas.
Cada clave lleva una lista de permisos (send, verify, status…). Fuera de esa lista: 403 FORBIDDEN.
Opcional, por clave. Una llamada desde otra IP recibe 401 UNAUTHORIZED.
X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset (tiempo Unix) en cada respuesta.
POST /v1/messages HTTP/1.1
Host: api.envoisms.ma
Authorization: Bearer smr_your_live_key
Content-Type: application/json
Idempotency-Key: 6f1d2c9e-4b7a-4f3e-9d21-0c5e8a7b3f10
HTTP/1.1 202 Accepted
X-RateLimit-Limit: 180
X-RateLimit-Remaining: 179
X-RateLimit-Reset: 1792663860Confirmación de pedido (SMS)
Enviar una confirmación de pedido por SMS#
Actualizado el
- Cree una clave sandbox y expórtela: export ENVOISMS_API_KEY=env_test_…
- Opcional: solicite su nombre de remitente (POST /v1/sender-ids, 15 a 30 días). Mientras tanto, omita sender_id: se aplica el remitente por defecto.
- En su evento «pedido pagado», envíe el SMS con Idempotency-Key = el id del pedido: un disparo doble envía un solo mensaje.
- Registre un webhook para message.delivered y message.failed; metadata.order_id vuelve en cada entrega.
- Pruebe +212600000002 para ver la rama de fallo y luego cambie a una clave live (smr_…).
curl https://api.envoisms.ma/v1/messages \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-paid" \
-d '{
"to": "+212612345678",
"message": "Commande #1042 confirmée : 349 MAD. Livraison prévue jeudi.",
"metadata": { "order_id": "1042" }
}'Pedidos COD en WhatsApp
Confirmar pedidos contra reembolso por WhatsApp#
Actualizado el
En WooCommerce, el plugin de EnvoiSMS (v2.0.0) lo hace todo: cada pedido COD espera, el cliente recibe un mensaje con los botones Confirmar / Cancelar, y su toque actualiza el pedido.
- Conecte su número de WhatsApp en Panel → WhatsApp (puede seguir usando la app WhatsApp Business en su teléfono).
- Instale el plugin desde github.com/envoisms/envoisms-woocommerce (WooCommerce 7.1+, PHP 8.0+) y pegue su clave API en WooCommerce → EnvoiSMS Maroc.
- Marque COD «Activer» y guarde: el plugin registra su webhook (message.inbound) y guarda el secreto de firma.
- Pulse «Créer les modèles WhatsApp» (francés y árabe): EnvoiSMS las revisa y las envía a Meta; su estado aparece en la misma página.
- Cuando las plantillas estén aprobadas, haga un pedido de prueba contra reembolso. Puede seguir su estado por la API:
curl "https://api.envoisms.ma/v1/templates?channel=whatsapp" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"Campaña de marketing (API)
Lanzar una campaña de marketing por la API#
Actualizado el
- Cree una lista: POST /v1/contacts/lists {"name": "Clientes fieles"} → lst_…
- Importe los contactos: POST /v1/contacts/import {"list_id", "contacts": [{"phone", "name"}]}.
- Registre la prueba del consentimiento de cada uno: POST /v1/consents {"phone", "purpose": "marketing", "status": "granted", "source": "form", "evidence": {…}}.
- Cree la campaña: SMS con body (indique cómo darse de baja); WhatsApp con template_id, template_variables y "consent_attested": true. scheduled_at la programa.
- Lánzela (POST /v1/campaigns/{id}/send), sígala con GET /v1/campaigns/{id}, páusela con /pause. Los números dados de baja (STOP) se excluyen automáticamente.
curl https://api.envoisms.ma/v1/campaigns \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Soldes de novembre",
"channel": "sms",
"list_id": "lst_…",
"body": "-20% ce week-end avec le code NOV20.",
"scheduled_at": "2026-11-07T09:00:00Z"
}'Bandeja bidireccional (webhooks)
Una bandeja de soporte bidireccional#
Actualizado el
- Registre un webhook para message.inbound (y message.read si muestra confirmaciones de lectura).
- Verifique la firma y archive el mensaje por número: data.from es el cliente, data.channel el canal.
- Lea el historial de un cliente con GET /v1/conversations/{phone}.
- Responda con POST /v1/conversations/{phone}/messages. En WhatsApp, el texto libre solo es posible en las 24 h siguientes al último mensaje del cliente; después, envíe una plantilla (template_name) o pase a SMS.
- Marque el hilo como leído con POST /v1/conversations/{phone}/mark-read. Un STOP del cliente llega como contact.optout y bloquea los envíos siguientes.
curl https://api.envoisms.ma/v1/conversations/+212612345678/messages \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "Bonjour, votre colis part aujourd'"'"'hui." }'Plataformas y agencias
Conectar el WhatsApp de sus clientes#
Actualizado el
Un número de WhatsApp no se conecta por la API: Meta exige que el propietario del negocio inicie sesión él mismo (Embedded Signup). Por eso cada cliente tiene su propia cuenta de EnvoiSMS, que paga sus envíos.
- El cliente crea su cuenta en envoisms.ma (registro por e-mail).
- Conecta su número en Panel → WhatsApp iniciando sesión en Meta (Embedded Signup; coexistencia con la app WhatsApp Business posible).
- Crea una clave API limitada a lo que necesita (p. ej. send, status) y se la entrega; usted la guarda en el servidor, una por cliente.
- Compruebe que el número puede enviar: GET /v1/whatsapp/waba-connection → readiness.can_send y readiness.blockers.
- Cree sus plantillas (POST /v1/templates) y envíe con su clave. Sus envíos, su saldo y sus webhooks siguen siendo suyos.
curl https://api.envoisms.ma/v1/whatsapp/waba-connection \
-H "Authorization: Bearer $CLIENT_API_KEY"Sandbox y pruebas
Probar sin enviar ni pagar#
Actualizado el
Cree una clave sandbox en Panel → Claves API (marque «Sandbox»). Empieza por env_test_; una clave live empieza por smr_. Ambas llaman a la misma URL, https://api.envoisms.ma/v1: decide la clave, no la dirección.
Una clave sandbox se valida igual que una live (formato del número, Sender ID, ventana de 24 h, contenido), pero no se envía ni se cobra nada. Lo que una clave live habría rechazado se indica en warnings en lugar de ocultarse.
- POST /v1/messages devuelve sandbox: true, cost a cero y simulated_cost: el precio real del mismo envío.
- POST /v1/verify/send devuelve sandbox_code: el código que habría recibido el usuario.
- Cada clave solo ve su propio tráfico: GET /v1/messages con una clave sandbox solo lista los envíos simulados, y a la inversa.
- Números de prueba (POST /v1/messages, solo clave sandbox): +212600000002 falla (message.failed, SANDBOX_TEST_FAILED); +212600000003 no es entregable (message.undeliverable); +212600000004 está dado de baja (403 OPTED_OUT en la petición); +212600000005 nunca recibe acuse (queda «sent»); cualquier otro número, incluido +212600000001, se entrega (message.delivered) unos 2 s después del envío.
- Una clave sandbox no puede convertirse en live: cree una clave live cuando esté listo y cambie la variable de entorno.
¿SMS o WhatsApp?
Elegir el canal#
Actualizado el
SMS es el canal por defecto: omita channel. Llega a cualquier teléfono, sin número conectado, sin plantilla y sin ventana de 24 h. Es la opción correcta para una notificación o alerta a cualquier cliente.
WhatsApp ("channel": "whatsapp") requiere su propio número de WhatsApp Business conectado. Para escribir primero, o más de 24 h después del último mensaje del cliente, use una plantilla aprobada; el texto libre solo se acepta en las 24 h siguientes a un mensaje del cliente (si no, 400 OUT_OF_24H_WINDOW, nada cobrado). WhatsApp se factura según la categoría de la plantilla.
Ambos a la vez: "cascade": true prueba WhatsApp y pasa a SMS si WhatsApp rechaza, falla o no confirma la entrega en cascade_timeout segundos (120 por defecto). Solo se factura lo que realmente salió.
Códigos de verificación: prefiera POST /v1/verify/send, que gestiona generación, intentos, caducidad y reenvío, en lugar de enviar su propio código por /v1/messages.
Idempotencia y reintentos
Reintentar sin enviar dos veces#
Actualizado el
Añada una cabecera Idempotency-Key (hasta 255 caracteres, p. ej. un UUID o «pedido-1042-enviado») a POST /v1/messages y POST /v1/messages/bulk (y sus equivalentes /v1/waba/messages). Si la llamada expira o su proceso falla, repita la misma petición con la misma clave: el envío ocurre una sola vez y el reintento devuelve el cuerpo JSON original con Idempotency-Replayed: true.
- La clave se conserva 24 h, por cuenta. Toda respuesta inferior a 500 se guarda y se repite, errores 4xx incluidos (p. ej. 429 o 402): tras una 4xx, corrija la causa y use una clave NUEVA.
- La clave está ligada al cuerpo exacto, byte a byte: un reintento reenvía el mismo cuerpo. Otro cuerpo con la misma clave — incluso el mismo JSON con otro formato — devuelve 422 IDEMPOTENCY_KEY_REUSED.
- Un 5xx o un tiempo agotado no se guarda: reintente con la MISMA clave, con espera creciente (1 s, 2 s, 4 s…).
- 409 IDEMPOTENCY_IN_FLIGHT: la petición original sigue en curso; espere Retry-After y reintente con la misma clave. 422 IDEMPOTENCY_KEY_REUSED: la clave ya se usó con otro cuerpo.
- 429 RATE_LIMITED: espere los segundos indicados en Retry-After. X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset acompañan a cada respuesta autenticada.
Paginación
Recorrer una lista#
Actualizado el
Las listas aceptan limit y offset y devuelven { "data": [...], "limit", "offset", "total" }. Para GET /v1/messages: 50 por defecto, 200 como máximo, del más reciente al más antiguo, filtrable por status, channel, campaign_id, from_date y to_date. Añada format=csv para obtener todo el conjunto filtrado en un archivo.
curl "https://api.envoisms.ma/v1/messages?status=failed&limit=100&offset=0" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"Permisos de claves
Una clave, el mínimo acceso#
Actualizado el
Cada clave lleva permisos. Una llamada fuera de ellos devuelve 403 FORBIDDEN. Dé a cada servidor la lista más pequeña que funcione: un backend de OTP solo necesita verify y status.
- send — POST /v1/messages, /v1/messages/bulk, /v1/templates, /v1/conversations, /v1/lookup
- status — GET /v1/messages, POST /v1/verify/check y el resto de /v1/verify (sessions, apps, lookup, void)
- verify — POST /v1/verify/send, /v1/verify/resend
- balance — GET /v1/billing/balance; billing — el resto de /v1/billing
- contacts (contactos, listas, consentimientos), campaigns, webhooks, optouts, analytics, api_keys, sender_id
- Configuración de WhatsApp (/v1/whatsapp/…): los cambios — conectar un número, herramientas, perfil, relés — exigen una sesión del panel (una clave API recibe 403). Solo la lectura está abierta a las claves, p. ej. GET /v1/whatsapp/waba-connection.
Versiones y cambios
Qué puede cambiar y cómo lo sabrá#
Actualizado el
La versión está en la ruta: /v1. Dentro de /v1 la API solo cambia por adición: nuevos endpoints, campos opcionales, campos de respuesta, códigos de error, eventos de webhook. Ignore los campos que no conozca. Un cambio incompatible sale como /v2 junto a /v1, nunca en su lugar.
Retirar algo de /v1 se anuncia con al menos 12 meses de antelación: por e-mail al titular de la cuenta, en el registro de cambios y en la propia ruta, cuyas respuestas llevan entonces Deprecation (fecha del anuncio), Sunset (fecha de fin) y Link a la guía de migración. La única excepción es una corrección de seguridad, anunciada lo antes posible. Ninguna ruta de /v1 está obsoleta hoy.
Límites por endpoint
Límites, endpoint por endpoint#
Actualizado el
Todas las rutas comparten el límite por minuto de la clave (por defecto Starter 60, Business 180, Pro 500, Enterprise 1 200; X-RateLimit-* en cada respuesta; 429 RATE_LIMITED con Retry-After por encima). Algunas rutas añaden reglas propias:
- POST /v1/messages/bulk: 10 000 mensajes por llamada.
- POST /v1/messages y /bulk: cuotas diaria y mensual opcionales de la cuenta (PATCH /v1/billing/quota); por encima, 429 QUOTA_EXCEEDED hasta medianoche UTC.
- POST /v1/verify/send y /resend: por número, un código cada 30 s por SMS y 60 s por WhatsApp (429 OTP_COOLDOWN), 5 por hora y 10 por día (429 OTP_RATE_LIMIT); 200 por día y cuenta; fuera de Marruecos, OTP_COUNTRY_PAUSED cuando se confirman muy pocos.
- POST /v1/verify/check: 3 intentos por sesión por defecto (configurables por app Verify), luego 429 MAX_ATTEMPTS.
- POST /v1/inbound-webhooks/catch/{id} (sin clave): 120 peticiones por minuto por webhook, 64 KB por petición.
- Sus webhooks: esperamos su respuesta 10 s como máximo y reintentamos 3 veces (1 min, 10 min, 1 h).
Webhooks
Recibir estados y respuestas#
Actualizado el
Cree un webhook con POST /v1/webhooks ({"url": "https://…", "events": ["message.delivered", "message.failed"]}). La respuesta incluye el secreto de firma, mostrado una sola vez; POST /v1/webhooks/{id}/rotate-secret emite uno nuevo. events acepta un nombre exacto, un espacio de nombres ("message.*") o "*".
Cada entrega es un POST JSON {"event", "data", "timestamp"}. Para un estado de mensaje, data es {id, to, channel, status, error_code?, error_message?, metadata?} — metadata es la que usted envió. Responda 2xx antes del timeout de 10 segundos y procese de forma asíncrona; deduplique por data.id + event, ya que una entrega puede llegar dos veces.
Verifique X-EnvoiSMS-Signature-256: t=<segundos>,v1=<hex>: calcule HMAC-SHA256(secreto, t + "." + cuerpo bruto), compárelo con v1 en tiempo constante y rechace si t tiene más de 5 minutos. La cabecera antigua X-EnvoiSMS-Signature (sha256=<hex> del cuerpo solo, sin marca de tiempo) aún se envía; prefiera la nueva, que impide la repetición. El evento también va en X-EnvoiSMS-Event.
- Reintentos: un 5xx, un 429 o un error de red se reintenta 3 veces (tras 1 min, 10 min y 1 h: 4 intentos en total). Otro 4xx no se reintenta.
- Registro y repetición: GET /v1/webhooks/logs lista los últimos 100 intentos; POST /v1/webhooks/logs/{log_id}/replay reenvía una entrega, firmada de nuevo. POST /v1/webhooks/{id}/test envía un ejemplo.
- Relé de WhatsApp a su servidor (número conectado en modo «webhook» en el panel): los mensajes y estados llegan en el formato nativo de WhatsApp Cloud, firmados con X-Hub-Signature-256: sha256=<hex> = HMAC-SHA256(webhook_secret del número, cuerpo bruto). Ese secreto se muestra una vez, al crearse o renovarse (rotate_webhook_secret). Sin secreto no se reenvía nada. No hay marca de tiempo: deduplique por el id del mensaje.
Eventos
message.sentEl mensaje se entregó a la red (operador o WhatsApp).
message.deliveredEl teléfono del destinatario confirmó la recepción.
message.readWhatsApp: el destinatario leyó el mensaje (salvo que haya desactivado las confirmaciones de lectura).
message.failedEl envío falló; error_code y error_message indican el motivo (ver Códigos de fallo de entrega).
message.undeliverableLa red confirmó que el mensaje no puede entregarse (número inexistente, caducado en el operador).
message.fallbackCascada: el mensaje pasa al siguiente canal. channel = canal nuevo, previous_channel = el anterior, error_code = el motivo.
message.inboundUn contacto le escribió (respuesta SMS o mensaje de WhatsApp, incluido un toque en botón o lista).
message.reactionWhatsApp: un contacto reaccionó a uno de sus mensajes con un emoji.
contact.optout / contact.optinUn contacto se dio de baja (STOP) o volvió a suscribirse. Los envíos a un número dado de baja se rechazan (403 OPTED_OUT).
lead.qualifiedEl bot de calificación de WhatsApp terminó de calificar un cliente potencial.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Keep the RAW body: the signature is computed over the exact bytes received.
app.post('/webhooks/envoisms', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-EnvoiSMS-Signature-256') || '';
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', process.env.ENVOISMS_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const valid = parts.v1?.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!fresh || !valid) return res.sendStatus(401);
const { event, data } = JSON.parse(req.body);
res.sendStatus(200); // answer fast…
queue.push({ event, data }); // …then process (de-duplicate on data.id + event)
});POST /webhooks/envoisms HTTP/1.1
Content-Type: application/json
X-EnvoiSMS-Event: message.delivered
X-EnvoiSMS-Signature-256: t=1792663800,v1=5f0c…e9
X-EnvoiSMS-Signature: sha256=a41b…7d
User-Agent: EnvoiSMS-Webhook/1.0 (formerly SMSMaroc)
{
"event": "message.delivered",
"data": {
"id": "msg_9f2c4e1a7b3d",
"to": "+212612345678",
"channel": "sms",
"status": "delivered",
"metadata": { "order_id": "1042" }
},
"timestamp": "2026-10-22T10:30:04.000Z"
}Errores
Errores#
Un error devuelve un estado 4xx o 5xx y siempre el mismo sobre. Bifurque por error.code, nunca por el texto: error.message es una frase en inglés cuya redacción puede mejorar. Cada código indica qué hacer.
HTTP/1.1 400 Bad Request
{
"error": {
"code": "INVALID_PHONE",
"message": "to must be E.164 format, for example +212612345678",
"docs": "https://envoisms.ma/en/docs#errors"
}
}Autenticación y acceso
UNAUTHORIZEDHTTP 401Falta la cabecera Authorization, la clave es desconocida o está revocada, la IP no está en la lista permitida de la clave, o la cuenta no está activa; error.message indica cuál. Envíe Authorization: Bearer <clave> y compruébela en Panel → Claves API.
FORBIDDENHTTP 403La clave no tiene permiso para esta ruta (ver Permisos de claves), o la ruta exige una sesión del panel. Añada el permiso a la clave o use otra.
ACCOUNT_SUSPENDEDHTTP 403La cuenta está suspendida. Contacte con soporte; no se puede enviar nada hasta su revisión.
WORKSPACE_NOT_FOUNDWORKSPACE_SUSPENDEDAPI_KEY_WORKSPACE_MISMATCHHTTP 404 / 403La organización especificada en la cabecera x-workspace-id no existe, está suspendida o entra en conflicto con la clave API.
FORBIDDEN_ASSETHTTP 403Ese número de WhatsApp no pertenece a su cuenta. Use un phone_number_id de sus propios números conectados.
PAYMENT_NOT_SETTLEDINVALID_CREDENTIALMACHINE_PAYMENTS_DISABLEDHTTP 402 / 400 / 501Pagos de máquina (Authorization: Payment …): el depósito aún no se ha confirmado (espere y reintente), la credencial no se decodifica o la función está desactivada.
Petición mal formada
INVALID_JSONHTTP 400El cuerpo no es JSON válido. Envíe Content-Type: application/json y un objeto JSON.
MISSING_FIELDMISSING_FIELDSINVALID_FIELDMISSING_BODYMISSING_MESSAGEMISSING_URLHTTP 400Falta un campo obligatorio o su valor está fuera de rango; error.message nombra el campo. Corríjalo y reenvíe.
INVALID_PHONEHTTP 400El número no está en formato E.164. Envíe + prefijo y el número, sin espacios: +212612345678.
INVALID_URLHTTP 400Una URL no es https:// o no es válida (webhook, medio, botón). Use una URL https pública.
INVALID_CHANNELCHANNEL_NOT_CONFIGUREDCHANNEL_DISABLEDUNSUPPORTED_CHANNELHTTP 400Canal desconocido o no disponible. En /v1/messages, channel es sms (por defecto) o whatsapp; en /v1/consents, whatsapp, sms o any.
INVALID_PURPOSEINVALID_STATUSINVALID_SOURCEINVALID_EVIDENCEINVALID_DATEHTTP 400Consentimientos: purpose = marketing | transactional | otp | service; status = granted | withdrawn; source = api | form | import | dashboard; evidence = objeto JSON < 4 KB; fechas ISO 8601, recorded_at nunca en el futuro.
INVALID_PERMISSIONSSANDBOX_IMMUTABLEHTTP 400Claves API: indique al menos un permiso válido; una clave nunca cambia entre sandbox y live — cree una nueva.
NOT_FOUNDLIST_NOT_FOUNDLEAD_NOT_FOUNDTEMPLATE_NOT_FOUNDAPP_NOT_FOUNDSHEET_NOT_FOUNDINVALID_MESSAGE_IDHTTP 400 / 404El recurso no existe para esta cuenta (o este modo de clave: una clave sandbox no ve objetos live), o el identificador solicitado no es válido (ej. null o indefinido). Compruebe el id y la clave.
METHOD_NOT_ALLOWEDHTTP 405Ese método HTTP no existe en esta ruta. Consulte la referencia del endpoint.
UNSUPPORTED_MEDIA_TYPEPAYLOAD_TOO_LARGEFILE_TOO_LARGEINVALID_FILE_TYPEINVALID_FORM_DATAMISSING_FILEEMPTY_SHEETPARSE_FAILEDMAPPING_ERRORFETCH_FAILEDINVALID_PATHNO_FILE_PROVIDEDNO_DATA_PROVIDEDUPLOAD_FAILEDHTTP 400 / 413 / 415 / 502Archivo o importación rechazados: tipo no admitido, demasiado grande, vacío, columnas no reconocidas o URL inaccesible. error.message indica cuál; corrija el archivo y reenvíe.
Envío (SMS y común)
INSUFFICIENT_BALANCEHTTP 402Saldo insuficiente para este envío. Recargue en Panel → Facturación; no se cobró nada.
INTERNATIONAL_REQUIRES_TOPUPHTTP 403Los envíos fuera de Marruecos son para cuentas que ya han recargado. Haga una primera recarga.
QUOTA_EXCEEDEDHTTP 429Se alcanzó la cuota diaria fijada en su cuenta; se reinicia a medianoche UTC. Auméntela en el panel si es necesario. Tras este 429, reintente con una nueva Idempotency-Key.
OPTED_OUTHTTP 403Este destinatario se dio de baja de sus mensajes (STOP). No reenvíe; quítelo de sus listas. Puede volver a suscribirse él mismo.
SENDER_ID_TOO_LONGSENDER_ID_TOO_SHORTSENDER_ID_INVALIDHTTP 400Un Sender ID alfanumérico tiene de 3 a 11 caracteres (letras, cifras, espacio). Corríjalo u omítalo para usar el remitente por defecto.
SENDER_ID_GENERICHTTP 400Sender ID genérico (INFO, ALERT, SERVICE…), no una marca. Use su nombre de marca u omítalo.
SENDER_ID_PROTECTED_BRANDHTTP 403El Sender ID nombra o imita a una institución protegida (banco, operador, administración). Reservado a la cuenta aprobada para ese nombre exacto.
SENDER_ID_NOT_APPROVEDSENDER_ID_PENDINGSENDER_ID_IN_REVIEWSENDER_ID_REJECTEDSENDER_ID_APPROVEDHTTP 403 / 409Registro de Sender ID: aún no aprobado, en revisión, rechazado o ya aprobado (409). Siga su estado en Ajustes → Sender IDs.
SENDER_ID_NOT_REGISTEREDHTTP 403Ya no se devuelve desde el 27/09/2026: un Sender ID no registrado se envía automáticamente con la tarifa estándar. Se mantiene para que el código que lo comprueba siga siendo válido.
SENDER_NOT_ASSIGNED_TO_WORKSPACEHTTP 403El Sender ID especificado no está asignado a este espacio de trabajo. Asígnelo en Ajustes → Espacios de trabajo.
NO_SENDER_ID_ASSIGNEDHTTP 422No hay ningún Sender ID asignado a este espacio de trabajo. Asigne un Sender ID aprobado en Ajustes → Espacios de trabajo.
WORKSPACE_BALANCE_EXHAUSTEDHTTP 402El saldo asignado a este espacio de trabajo se ha agotado. Asigne más saldo desde la cuenta principal en Ajustes → Espacios de trabajo.
SPAM_OR_PHISHING_DETECTEDCONTENT_BLOCKEDHTTP 400Contenido rechazado: enlace de phishing, mensaje en nombre de un banco, operador o administración, o violación del uso aceptable. La cuenta queda suspendida pendiente de revisión. Recurso: POST /v1/moderation/appeals con error.appeal.id, error.appeal.token y una nota (sin clave API).
CONTENT_REFUSEDHTTP 400Contenido rechazado por el control anti-abuso (primer rechazo). No se envió nada y la cuenta sigue activa; un segundo rechazo la suspende. Reformule o recurra vía POST /v1/moderation/appeals.
INVALID_APPEALAPPEAL_NOT_FOUNDAPPEAL_ALREADY_FILEDHTTP 400 / 404 / 409Recurso: id o token ausente o erróneo, o ya hay un recurso abierto para este rechazo.
EMPTY_BATCHTOO_MANY_MESSAGESHTTP 400POST /v1/messages/bulk acepta de 1 a 10 000 mensajes por llamada. Divida los lotes mayores.
SEND_PERSIST_FAILEDHTTP 502No se pudo registrar el mensaje; no se envió ni se cobró nada. Reintente con la misma Idempotency-Key.
WHATSAPP_NOT_CONNECTEDHTTP 403No hay ningún número de WhatsApp Business conectado. Conéctelo en el panel; para un texto simple, omita channel (SMS). No se cobró nada.
OUT_OF_24H_WINDOWHTTP 400Mensaje libre de WhatsApp a un contacto que no le escribió en 24 h. Envíe una plantilla aprobada (template) o un SMS. No se cobró nada.
CONTACT_HAS_NO_PHONEHTTP 400Este contacto usa un nombre de usuario de WhatsApp y no ha compartido su número: solo se le puede contactar por WhatsApp, no por SMS. No se cobró nada.
WHATSAPP_ONLY_FIELDBUTTONS_NOT_SUPPORTEDINTERACTIVE_NOT_SUPPORTEDHTTP 400Se envió por SMS un campo exclusivo de WhatsApp (buttons, interactive, reaction, contacts, location, sticker, context). Añada "channel": "whatsapp" o quite el campo.
CONFLICTING_MESSAGE_TYPESCASCADE_NOT_SUPPORTEDINVALID_MESSAGEHTTP 400Un mensaje lleva un solo tipo de contenido; reaction, contacts, location y sticker no se combinan con nada y no tienen equivalente SMS (sin cascade).
INVALID_REACTIONINVALID_CONTEXTINVALID_CONTACTSINVALID_LOCATIONINVALID_MEDIAHTTP 400Objeto de WhatsApp mal formado: reaction = {message_id, emoji}; context = {message_id}; contacts = 1–20 fichas con name.formatted_name; location = latitude/longitude numéricas; medio = "id" O "link" https, sin pie en audio/sticker.
INVALID_INTERACTIVEINVALID_INTERACTIVE_TYPEMISSING_INTERACTIVE_ACTIONINTERACTIVE_BODY_TOO_LONGINVALID_CTA_URL_ACTIONMISSING_CTA_DISPLAY_TEXTCTA_DISPLAY_TEXT_TOO_LONGINVALID_LOCATION_REQUEST_ACTIONINVALID_FLOW_ACTIONMISSING_FLOW_IDMISSING_FLOW_CTAFLOW_CTA_TOO_LONGMISSING_CAROUSEL_CARDSTOO_MANY_CAROUSEL_CARDSINVALID_PRODUCT_ACTIONINVALID_PRODUCT_LISTMISSING_ORDER_ACTIONMISSING_ORDER_CURRENCYMISSING_ORDER_ITEMSMISSING_ORDER_PARAMETERSMISSING_REFERENCE_IDINVALID_TOTAL_AMOUNTMISSING_PHONE_NUMBERHTTP 400Mensaje interactive mal formado (lista, botones, cta_url, flow, carrusel, producto, pedido, location_request). error.message nombra el campo erróneo y el límite de WhatsApp superado.
MISSING_BUTTONS_ARRAYINVALID_BUTTONINVALID_BUTTON_COUNTINVALID_BUTTON_TEXTINVALID_BUTTON_TYPEINVALID_REPLY_BUTTONBUTTON_TEXT_TOO_LONGBUTTON_TITLE_TOO_LONGTOO_MANY_BUTTONSMISSING_COPY_CODEMISSING_LIST_BUTTONLIST_BUTTON_TOO_LONGMISSING_LIST_SECTIONSTOO_MANY_SECTIONSSECTION_TITLE_TOO_LONGINVALID_SECTION_ROWSTOO_MANY_ROWSMISSING_ROW_IDMISSING_ROW_TITLEROW_ID_TOO_LONGROW_TITLE_TOO_LONGROW_DESC_TOO_LONGHTTP 400Botones o lista fuera de los límites de WhatsApp: máx. 3 botones, títulos de botón de 20 caracteres, 10 secciones y 10 filas, títulos de fila de 24 caracteres, descripción de 72. error.message da el límite exacto.
META_SERVICE_UNAVAILABLEHTTP 502WhatsApp no estuvo disponible momentáneamente y el envío falló tras los reintentos automáticos. Reintente en unos minutos.
Plantillas y campañas
TEMPLATE_PENDING_REVIEWTEMPLATE_REJECTED_BY_REVIEWHTTP 409La plantilla espera la revisión de EnvoiSMS (pasa a Meta una vez aprobada), o esa revisión la rechazó: edítela para volver a enviarla a revisión.
TEMPLATE_EXISTSTEMPLATE_ALREADY_ON_METATEMPLATE_EDIT_NOT_ALLOWEDTEMPLATE_IMMUTABLE_FIELDHTTP 400 / 409Ya existe una plantilla con ese nombre, ya está en Meta (sincronícela), no se puede editar en su estado actual, o cambia un campo que Meta fija (nombre, idioma, categoría de una plantilla aprobada) — cree una nueva.
INVALID_LANGUAGEINVALID_META_TEMPLATEINVALID_AUTH_TEMPLATEMISSING_SAMPLE_VALUESMISSING_HEADER_EXAMPLEINVALID_HEADER_EXAMPLEHEADER_EXAMPLE_INVALID_SIZEHEADER_EXAMPLE_INVALID_TYPEHEADER_EXAMPLE_UNREACHABLEHTTP 400La plantilla no cumple las reglas de Meta: idioma desconocido, faltan ejemplos de variables, medio de cabecera ausente, inaccesible, de tipo incorrecto o demasiado grande, o plantilla de autenticación no conforme. error.message lo precisa.
INVALID_WABAWHATSAPP_TOKEN_EXPIREDMETA_API_ERRORMETA_SUBMIT_FAILEDMETA_UPLOAD_FAILEDMETA_NOT_CONFIGUREDHTTP 400 / 502 / 503Meta rechazó la operación en su cuenta de WhatsApp Business: waba_id ajeno, token de conexión caducado (reconecte la cuenta en el panel) o error de Meta. Reintente; si persiste, contacte con soporte.
TEMPLATE_NOT_APPROVEDMISSING_TEMPLATEMISSING_TEMPLATE_PARAMSHTTP 400Campaña o conversación de WhatsApp: la plantilla no está aprobada, falta, o faltan variables. MISSING_TEMPLATE_PARAMS lista lo que debe indicar en template_variables (_header_media_url, _button_<i> o "static:valor").
CONSENT_ATTESTATION_REQUIREDHTTP 400Una campaña de marketing por WhatsApp exige "consent_attested": true: usted confirma que cada contacto de la lista aceptó recibir sus mensajes de WhatsApp. Envíelo solo si es cierto.
INVALID_WABA_SENDERMISSING_LISTEMPTY_LISTLIST_IN_USEHTTP 400Campaña: el remitente de WhatsApp no está conectado a su cuenta, falta la lista o no tiene destinatarios válidos, o la lista la usa una campaña activa (y no se puede eliminar).
ALREADY_RUNNINGCAMPAIGN_RUNNINGCAMPAIGN_NOT_EDITABLENOT_PAUSEDNOT_RUNNINGHTTP 400 / 409El estado de la campaña no permite esta acción: solo se edita una campaña en borrador o programada, y una campaña en curso debe pausarse antes de eliminarla.
Verificación (OTP)
INVALID_CODEHTTP 400Código erróneo. error.message indica los intentos restantes; deje que el usuario lo intente de nuevo.
MAX_ATTEMPTSHTTP 429Demasiados intentos: la sesión está cerrada. Inicie una nueva con /v1/verify/send.
SESSION_NOT_FOUNDSESSION_EXPIREDINVALID_STATEINVALID_SESSION_DATAINVALID_SESSION_IDHTTP 400 / 404 / 409Sesión desconocida, caducada, ID no válido (ej. null o indefinido), ya verificada o anulada, o creada por una clave del otro modo (sandbox/live). Inicie una nueva verificación con la misma clave para send, resend y check.
OTP_COOLDOWNOTP_RATE_LIMITHTTP 429Se acaba de enviar un código a este número (30 s por SMS, 60 s por WhatsApp), o se enviaron demasiados en la última hora. Espere Retry-After; ofrezca «Reenviar» con /v1/verify/resend.
OTP_RESEND_LIMIT_EXCEEDEDHTTP 429Límite máximo de reintentos de reenvío (4 envíos) alcanzado para esta sesión de verificación. Espere a que la sesión caduque antes de iniciar una nueva.
OTP_COUNTRY_PAUSEDHTTP 429Códigos a ese país (fuera de Marruecos) suspendidos para su cuenta hasta las 00:00 UTC: se confirmaron muy pocos de los de hoy, señal de tráfico automatizado. Retry-After indica los segundos restantes.
COUNTRY_NOT_ALLOWEDHTTP 403Los códigos a ese país no están abiertos para su cuenta o su app Verify. Contacte con soporte para abrirlo.
WHATSAPP_UNAVAILABLEDELIVERY_FAILEDINVALID_CODE_TYPEBAD_GATEWAYHTTP 400 / 502 / 503El código no pudo salir por WhatsApp (no disponible para la cuenta, rechazado o código no numérico). No se cobró nada; reenvíe con "channel": "sms".
HLR_UNAVAILABLEHTTP 503La consulta HLR (/v1/hlr) no pudo realizarse: servicio momentáneamente no disponible o limitado. Reintente en unos segundos; no se cobró nada. Un 502 BAD_GATEWAY en esta ruta significa que la red no respondió — misma regla, no se cobra nada.
LOOKUP_FAILEDLOOKUP_UNAVAILABLEHTTP 500 / 503La búsqueda de operador (/v1/verify/lookup) falló o no está disponible. Reintente más tarde; no se cobró nada.
Límites e idempotencia
RATE_LIMITEDTOO_MANY_REQUESTSRATE_LIMIT_EXCEEDEDHTTP 429Se superó el límite de peticiones por minuto de la clave. Espere Retry-After (segundos); reintentar antes solo consume la siguiente ventana.
INVALID_IDEMPOTENCY_KEYHTTP 400Idempotency-Key supera los 255 caracteres. Use un UUID o un identificador de negocio corto.
IDEMPOTENCY_IN_FLIGHTHTTP 409La petición original con esta clave sigue en curso. Espere Retry-After y reintente con la misma clave.
IDEMPOTENCY_KEY_REUSEDHTTP 422Esta Idempotency-Key ya se usó con otro cuerpo de petición. Una clave designa una sola petición: genere una nueva.
REPLAY_UNAVAILABLEHTTP 422Esta entrega de webhook no tiene contenido guardado para reenviar. No se puede repetir.
Cuenta y facturación
LIMIT_EXCEEDEDHTTP 403Se alcanzó el número máximo de claves API o webhooks de su plan. Revoque uno o cambie de plan.
INVALID_AMOUNTINVALID_PACKINVALID_PAYMENT_METHODUNSUPPORTED_METHODALREADY_COMPLETEDTRANSACTION_CANCELLEDRECEIPT_NOT_AVAILABLEINVOICE_NOT_AVAILABLESTRIPE_INVOICES_UNAVAILABLENO_STRIPE_CUSTOMERINVALID_THRESHOLDINVALID_CYCLEINVALID_CYCLE_LIMITNO_PAID_TOPUPNO_FIELDSHTTP 400 / 403 / 404 / 409 / 410Recarga o recarga automática: importe, umbral, límite, ciclo, paquete o medio de pago no válido, se requiere primera recarga manual, transacción cancelada o ya completada, o factura no disponible (aún).
STRIPE_ERRORPORTAL_ERRORCUSTOMER_CREATION_FAILEDSTRIPE_NOT_CONFIGUREDPAYMENT_METHOD_NOT_CONFIGUREDNOT_AVAILABLEINVOICE_TEMPORARILY_UNAVAILABLEINVOICE_BEING_GENERATEDINVALID_SIGNATUREHTTP 401 / 500 / 502 / 503No se pudo obtener la página de pago o de factura de nuestro lado. No se cobró nada; reintente en un momento.
Lado servidor
INTERNAL_ERRORDB_ERRORSERVICE_UNAVAILABLEBAD_REQUESTHTTP 400 / 500 / 503Un error de nuestro lado. Reintente con espera creciente (y la misma Idempotency-Key para un envío); contacte con soporte si persiste.
Códigos de fallo de entrega
Un mensaje aceptado (202) aún puede fallar. Su error_code — en GET /v1/messages/{id} y en el webhook message.failed — es uno de estos. Un mensaje de WhatsApp rechazado antes de salir se reembolsa automáticamente.
META_WINDOW_EXPIREDMás de 24 h desde el último mensaje del contacto. Envíe una plantilla aprobada o un SMS.
META_UNDELIVERABLEEl número no es localizable en WhatsApp. Sin reintento automático: use SMS.
META_MARKETING_LIMITLímite de mensajes de marketing por destinatario de WhatsApp. No reenvíe de inmediato.
META_RATE_LIMITED / META_PAIR_RATE_LIMITEDSe superó el caudal del número, o demasiados mensajes a este contacto. Falló tras reintentos automáticos; reduzca el ritmo.
META_POLICY_BLOCKED / META_ACCOUNT_LOCKED / META_PAYMENT_ISSUE / META_PHONE_NOT_REGISTEREDEl número o la cuenta de WhatsApp emisora está restringida, bloqueada, sin medio de pago de Meta válido o no registrada. Revise WhatsApp Manager; sus fondos de EnvoiSMS no son la causa.
META_TEMPLATE_NOT_FOUND / META_TEMPLATE_PARAM_MISMATCH / META_TEMPLATE_PARAM_FORMATPlantilla inexistente en ese idioma o aún no aprobada, número de variables incorrecto, o variable con formato erróneo (medio de cabecera, botón).
META_UNSUPPORTED_TYPEEste tipo de mensaje no es compatible con WhatsApp para este destinatario.
META_GENERIC_ERROR / META_SERVICE_UNAVAILABLEError temporal de WhatsApp que persistió tras los reintentos automáticos. Reenvíe en unos minutos.
CASCADE_TIMEOUTCascada: sin acuse de entrega en cascade_timeout; el mensaje pasó al siguiente canal (evento message.fallback).
Límites
Límites#
Por clave y minuto. Valor por defecto de una clave nueva según el plan: Starter 60, Business 180, Pro 500, Enterprise 1 200. Por encima: 429 RATE_LIMITED con Retry-After.
Hasta 10 000 mensajes por llamada a POST /v1/messages/bulk.
Código de 4 a 8 dígitos (6 por defecto), válido de 60 s a 30 min (10 min por defecto). Reenvío posible tras 30 s por SMS, 60 s por WhatsApp.
Idempotency-Key de hasta 255 caracteres, conservada 24 h por cuenta.
SDK
Bibliotecas cliente#
Opcionales: cada ejemplo de esta página funciona con HTTP simple, sin dependencias.
npm install envoismspip install envoismscomposer require envoisms/envoisms-phpcomposer require envoisms/laravel-otpgo get github.com/n4ouri/envoisms-gonpx -y @envoisms/mcp-serverEnviar un mensaje
Pone en cola un SMS o un mensaje de WhatsApp a un destinatario y descuenta su precio. Responde 202 con el identificador del mensaje; el resultado de la entrega llega después por webhook o mediante GET /v1/messages/:id.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| to | string | Requerido | Número del destinatario en formato E.164 (+212612345678). |
| message | string | Requerido | Texto del mensaje, 1 600 caracteres como máximo (el exceso se corta). Alias: body. Opcional en WhatsApp cuando la petición lleva una plantilla, un medio, una reacción, contactos, una ubicación o un mensaje interactivo. |
| sender_id | string | Opcional | SMS: el nombre que ve el destinatario (letras, dígitos y espacios, de 3 a 11 caracteres; se rechaza un encabezado genérico como INFO o el nombre de un banco). Si se omite, el SMS sale con nuestro remitente compartido EnvoiSMS a 0,792 MAD. Su propio nombre funciona sin trámite a 1,70 MAD mientras no esté validado con su RC, y a la tarifa estándar una vez validado. WhatsApp: el número conectado que envía. Alias: from, sender. |
| channel | string | Opcional | "sms" (por defecto) o "whatsapp". "whatsapp" envía desde su propio número de WhatsApp Business conectado (si no, 403 WHATSAPP_NOT_CONNECTED). Salvo que sea una plantilla, un mensaje de WhatsApp solo se acepta si el contacto le escribió en las últimas 24 horas; si no, 400 OUT_OF_24H_WINDOW, sin cargo. |
| cascade | boolean | string[] | Opcional | true: intenta WhatsApp y pasa a SMS — de inmediato si WhatsApp rechaza el mensaje o informa de su fallo, o si no llega ningún acuse dentro de cascade_timeout. También acepta una lista ordenada, por ejemplo ["whatsapp", "sms"]. Solo se factura el envío que realmente salió; un WhatsApp aún sin acuse sí salió, así que se factura junto con su SMS de respaldo y se reembolsa si WhatsApp informa después de su fallo. |
| cascade_timeout | integer | Opcional | Con cascade: segundos de espera de un acuse de entrega antes del canal siguiente, de 30 a 43200. Por defecto: 120. Dé a su plantilla de WhatsApp un tiempo de vida (TTL) no mayor que este valor, para que un teléfono que vuelve a conectarse no reciba ambos mensajes. |
| template | object | Opcional | (WhatsApp) Plantilla aprobada de su cuenta: {"name", "language", "components"?}. Los valores de las variables del cuerpo van en metadata.variables. Único tipo de mensaje permitido fuera de la ventana de 24 horas; se tarifica según la categoría de la plantilla. |
| metadata | object | Opcional | Pares clave-valor guardados con el mensaje y devueltos en los webhooks (no en GET /v1/messages). Claves que lee la plataforma: variables (valores de la plantilla), media_url y media_type (encabezado multimedia de la plantilla), purpose: "otp" (ver notas). |
| buttons | array | Opcional | (WhatsApp) Hasta 3 botones {"type", "text"}; text de 25 caracteres como máximo. type "copy" exige copy_code, "url" exige url (http o https), "call" exige phone_number. |
| interactive | object | Opcional | (WhatsApp) Mensaje interactivo en el formato de WhatsApp: lista ("list"), botones de respuesta ("button", 1 a 3), formulario ("flow"), botón de enlace ("cta_url") o solicitud de ubicación ("location_request_message"). |
| image | video | audio | document | sticker | object | Opcional | (WhatsApp) Medio por "id" (medio ya subido) o por "link" (URL https), nunca ambos. "caption" en imagen, vídeo y documento; "filename" en documento. |
| reaction | object | Opcional | (WhatsApp) {"message_id": wamid, "emoji": "👍"} — reacciona a un mensaje de la conversación; un emoji vacío retira la reacción. No se factura. |
| contacts | array | Opcional | (WhatsApp) Fichas de contacto (20 como máximo): name.formatted_name obligatorio; phones, emails, urls, addresses, org, birthday opcionales. |
| location | object | Opcional | (WhatsApp) Ubicación: {"latitude", "longitude", "name"?, "address"?}. |
| context | object | Opcional | (WhatsApp) {"message_id": wamid} — responde citando un mensaje anterior. Válido en todos los tipos salvo reaction. |
| phone_number_id | string | Opcional | (WhatsApp) Identificador de Meta del número conectado que envía; por defecto su número predeterminado. |
- Permiso de clave API requerido: send.
- Idempotencia: envíe una cabecera Idempotency-Key (255 caracteres como máximo) para reintentar una petición sin enviar dos veces. Durante 24 h, la misma clave con el mismo cuerpo devuelve la respuesta original (cabecera Idempotency-Replayed: true); con otro cuerpo, 422 IDEMPOTENCY_KEY_REUSED; mientras la primera petición sigue en curso, 409 IDEMPOTENCY_IN_FLIGHT.
- Clave sandbox (env_test_…): no se envía ni se cobra nada; cost vale 0 y la respuesta añade "sandbox": true, "simulated_cost" (el precio que habría pagado una clave live) y, si procede, "warnings" (lo que se le habría rechazado a una clave live, como una ventana de 24 h cerrada).
- Códigos de un solo uso: /v1/verify/send gestiona todo el ciclo y sigue siendo la vía recomendada. Si genera sus propios códigos y los envía aquí, añada metadata: {"purpose": "otp"}: ante un fallo de entrega confirmado por la red hacia un número marroquí mientras el código tiene menos de 10 minutos, la plataforma lo reenvía una vez por otra ruta SMS, con el mismo identificador de mensaje y sin coste adicional.
- Errores frecuentes: 400 INVALID_PHONE, 400 OUT_OF_24H_WINDOW, 400 SENDER_ID_GENERIC, 402 INSUFFICIENT_BALANCE, 403 WHATSAPP_NOT_CONNECTED, 403 SENDER_ID_PROTECTED_BRAND. Una petición rechazada nunca se cobra.
- Formato: los saltos de línea (\n) funcionan en todos los canales. El SMS es texto plano; WhatsApp interpreta *negrita*, _cursiva_ y ~tachado~.
curl -X POST "https://api.envoisms.ma/v1/messages" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"message": "Your order #1042 has shipped."
}'{
"id": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
"to": "+212612345678",
"channel": "sms",
"cascade": false,
"status": "queued",
"cost": { "eur": 0.072, "mad": 0.792 },
"segments": 1,
"created_at": "2026-10-22T09:14:03.512Z"
}Envío masivo
Pone en cola hasta 10 000 mensajes en una llamada, cada uno con su destinatario y su texto. Responde 202 con un batch_id, los mensajes aceptados y, si los hay, los rechazados.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| messages | array | Requerido | Array de 1 a 10 000 objetos: "to" (E.164), "message" (o "body") y, opcionalmente, "metadata" y "buttons" (WhatsApp). |
| sender_id | string | Opcional | Remitente común a todo el lote (mismas reglas que POST /v1/messages). Alias: from, sender. |
| channel | string | Opcional | "sms" (por defecto) o "whatsapp". En WhatsApp el envío masivo no lleva plantilla: cada destinatario debe estar dentro de su ventana de 24 horas. |
- Permiso de clave API requerido: send.
- El lote se valida entero antes de enviar nada: una sola entrada inválida hace fallar la petición con 400 INVALID_MESSAGE, con la lista de las 20 primeras entradas erróneas (índice y regla). El saldo se comprueba para todo el lote (402 INSUFFICIENT_BALANCE).
- "rejected" enumera las entradas rechazadas una a una (baja, ventana de 24 h cerrada…) con su código; no se cobran. Solo aparece si las hay. "estimated_cost" es la estimación del lote antes de los rechazos.
- Se acepta Idempotency-Key, con las mismas reglas que POST /v1/messages.
curl -X POST "https://api.envoisms.ma/v1/messages/bulk" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+212611111111",
"message": "Your appointment is tomorrow at 10:00."
},
{
"to": "+212622222222",
"message": "Your appointment is tomorrow at 11:30."
}
]
}'{
"batch_id": "batch_9a3c5e7f1b2d4c6e8a0f2b4d",
"total": 2,
"channel": "sms",
"estimated_cost": { "eur": 0.144, "mad": 1.584 },
"messages": [
{ "id": "msg_1a2b3c4d5e6f7a8b9c0d1e2f", "to": "+212611111111", "status": "queued" },
{ "id": "msg_3c4d5e6f7a8b9c0d1e2f3a4b", "to": "+212622222222", "status": "queued" }
],
"rejected": [
{ "to": "+212633333333", "code": "OPTED_OUT", "message": "This number opted out of messages from your account" }
]
}Indicador de escritura de WhatsApp
Marca como leído un mensaje de WhatsApp recibido y muestra «escribiendo…» a su autor mientras prepara la respuesta (hasta que responda o unos 25 s). No se almacena ni se factura nada.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| message_id | string | Requerido | Identificador de WhatsApp (wamid) del mensaje recibido al que responde. |
| typing_indicator | boolean | Opcional | false envía solo la confirmación de lectura. Por defecto: true. |
| phone_number_id | string | Opcional | Número conectado que recibió el mensaje; por defecto su número predeterminado. |
- Permiso de clave API requerido: send.
- Con una clave sandbox no se transmite nada a WhatsApp y la respuesta es {"status": "simulated", "sandbox": true, "message_id", "typing_indicator"}.
curl -X POST "https://api.envoisms.ma/v1/messages/typing" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message_id": "wamid.HBgMMjEyNjEyMzQ1Njc4FQIAEhg",
"typing_indicator": true
}'{
"status": "ok",
"message_id": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg",
"typing_indicator": true
}Listar mensajes
Devuelve los mensajes de la cuenta, del más reciente al más antiguo, con filtros y paginación. format=csv exporta todo el conjunto filtrado.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| limit | integer | Opcional | Número de resultados: 50 por defecto, 200 como máximo. |
| offset | integer | Opcional | Número de resultados que se omiten (paginación). Por defecto: 0. |
| status | string | Opcional | queued, sent, delivered, read (WhatsApp), failed, undeliverable o unconfirmed. |
| channel | string | Opcional | sms o whatsapp. |
| campaign_id | string | Opcional | Mensajes de una campaña (cmp_…). |
| from_date | string | Opcional | Creados a partir de esta fecha (UTC). Escríbala AAAA-MM-DD: se compara con created_at, con formato "AAAA-MM-DD HH:MM:SS". |
| to_date | string | Opcional | Creados hasta esta fecha (UTC), misma comparación. Para incluir un día entero, pase el día siguiente. |
| format | string | Opcional | csv: descarga todo el conjunto filtrado (hasta el tope de exportación, indicado en el archivo) en lugar de una página. |
- Permiso de clave API requerido: status.
- Una clave solo ve los mensajes de su propio modo: una clave sandbox solo lista envíos simulados, una clave live solo envíos reales.
curl -X GET "https://api.envoisms.ma/v1/messages?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
"campaign_id": null,
"to": "+212612345678",
"channel": "sms",
"body": "Votre commande 4512 est expédiée.",
"sender_id": "MABOUTIQUE",
"unicode": 0,
"segments": 1,
"status": "delivered",
"error_code": null,
"error_message": null,
"cost_eur": 0.072,
"cost_mad": 0.792,
"scheduled_at": null,
"sent_at": "2026-10-22 09:14:05",
"delivered_at": "2026-10-22 09:14:09",
"read_at": null,
"failed_at": null,
"created_at": "2026-10-22 09:14:03",
"operator": "Maroc Telecom",
"sandbox": 0
}
],
"limit": 50,
"offset": 0,
"total": 1
}Consultar un mensaje
Devuelve un mensaje y su estado de entrega. 404 NOT_FOUND si el identificador no pertenece a la cuenta o al modo de la clave.
- Permiso de clave API requerido: status.
- Estados: queued, sent, delivered, read (WhatsApp), failed, undeliverable, unconfirmed. "unconfirmed" significa que el operador no confirmó ni negó la entrega; un acuse que llegue más tarde aún puede sustituirlo.
- El campo metadata enviado no se devuelve aquí; acompaña a los webhooks.
curl -X GET "https://api.envoisms.ma/v1/messages/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
"campaign_id": null,
"to": "+212612345678",
"channel": "sms",
"body": "Votre commande 4512 est expédiée.",
"sender_id": "MABOUTIQUE",
"unicode": 0,
"segments": 1,
"status": "delivered",
"error_code": null,
"error_message": null,
"cost_eur": 0.072,
"cost_mad": 0.792,
"scheduled_at": null,
"sent_at": "2026-10-22 09:14:05",
"delivered_at": "2026-10-22 09:14:09",
"read_at": null,
"failed_at": null,
"created_at": "2026-10-22 09:14:03",
"operator": "Maroc Telecom",
"sandbox": 0
}Listar plantillas
Devuelve las plantillas activas de la cuenta (SMS y WhatsApp), de la más reciente a la más antigua, sea cual sea su estado.
- Permiso de clave API requerido: send.
- "variables" es una cadena JSON que describe la estructura de la plantilla: variables, valores de ejemplo, encabezado, pie, botones, carrusel y revisión.
- Estados de WhatsApp: pending_admin (revisión de EnvoiSMS), pending_meta, approved, rejected, paused, disabled, in_appeal, flagged, deleted_on_meta. Una plantilla SMS queda approved al crearse.
curl -X GET "https://api.envoisms.ma/v1/templates" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "order_shipped",
"channel": "whatsapp",
"body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
"variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
"category": "utility",
"active": 1,
"created_at": "2026-10-20 14:02:11",
"status": "approved",
"rejection_reason": null,
"meta_template_id": "1203948576102938",
"language": "fr",
"quality_score": "GREEN",
"quality_updated_at": "2026-10-21 08:00:00",
"waba_id": null
}
]
}Crear una plantilla
Crea una plantilla SMS (utilizable de inmediato) o de WhatsApp (revisada por EnvoiSMS y luego enviada a Meta en su propia cuenta de WhatsApp Business). Responde 201 con la fila creada.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| name | string | Requerido | Nombre de la plantilla. Se pasa a minúsculas; todo carácter distinto de a-z, 0-9 y _ se convierte en _. |
| channel | string | Requerido | sms, whatsapp, telegram o any. |
| body | string | Requerido | Texto de la plantilla. WhatsApp: 1 024 caracteres como máximo; variables todas numeradas desde {{1}} y consecutivas, o todas con nombre en minúsculas ({{nombre}}); nunca al principio ni al final, nunca pegadas. No se exige para una plantilla de WhatsApp "authentication", cuyo texto fija Meta. |
| category | string | Opcional | marketing (por defecto), utility o authentication. otp equivale a authentication y transactional a utility. Meta puede reasignar la categoría en la revisión. |
| language | string | Opcional | (WhatsApp) Código de idioma de Meta, por ejemplo fr, ar, en_US, es. Por defecto: fr. |
| sample_values | object | string[] | Opcional | (WhatsApp) Un valor de ejemplo por variable, exigido por Meta: {"1": "Amine"} o ["Amine"]. Sin él, 400 MISSING_SAMPLE_VALUES. |
| variables | string[] | Opcional | (SMS) Nombres de las variables del texto, guardados tal cual. |
| header_type | string | Opcional | (WhatsApp) none (por defecto), text, image, video o document. Un encabezado text lleva header_text (60 caracteres, una variable como máximo, con header_sample); un encabezado multimedia exige header_example_url. |
| header_text | header_sample | header_example_url | string | Opcional | (WhatsApp) Texto del encabezado; valor de ejemplo de su variable; enlace https público a un archivo de ejemplo para un encabezado de imagen, vídeo o documento (400 MISSING_HEADER_EXAMPLE si falta). |
| footer | string | Opcional | (WhatsApp) Pie, 60 caracteres como máximo. |
| buttons | array | Opcional | (WhatsApp) Hasta 10 botones {"type", "text"} (25 caracteres). Tipos: quick_reply; url (con url http(s), y url_example si el enlace termina en una variable); phone_number (con phone_number); copy_code (con copy_code); flow (con flow_id, el identificador de uno de sus WhatsApp Flows publicados, y navigate_screen, "LEAD" por defecto). |
| carousel | object | Opcional | (WhatsApp, marketing o utility) {"header_type": "image" | "video", "cards": [...]}: de 2 a 10 tarjetas, cada una {"header_example_url" (https), "body" (160 caracteres, sin variable), "buttons" (1 o 2, quick_reply o url sin variable)}. Todas las tarjetas llevan los mismos botones. Una plantilla carrusel no tiene encabezado, pie ni botones propios. |
| add_security_recommendation | code_expiration_minutes | otp_type | supported_apps | mixed | Opcional | (WhatsApp, authentication) Aviso de seguridad de Meta (booleano); validez del código, de 1 a 90 minutos; COPY_CODE (por defecto), ONE_TAP o ZERO_TAP; supported_apps [{"package_name", "signature_hash" (11 caracteres)}], exigido para ONE_TAP y ZERO_TAP. ZERO_TAP exige además zero_tap_terms_accepted: true. |
| waba_id | string | Opcional | (WhatsApp) Cuenta de WhatsApp Business de destino cuando la cuenta tiene varias; debe ser una de sus cuentas conectadas (si no, 400 INVALID_WABA). |
- Permiso de clave API requerido: send.
- Revisión: una plantilla de WhatsApp se crea en pending_admin. Unos controles automáticos (suplantación de un banco, enlace de phishing, alojamiento de enlaces desechables, cuenta inactiva) se ejecutan al instante; si no encuentran nada, la plantilla se envía a Meta (pending_meta) y la respuesta ya lo muestra. Si no, queda en pending_admin para una revisión humana. Solo Meta la aprueba (approved) y fija su categoría final.
- Errores: 400 MISSING_FIELD, INVALID_CHANNEL, INVALID_LANGUAGE, INVALID_META_TEMPLATE, MISSING_SAMPLE_VALUES, MISSING_HEADER_EXAMPLE, INVALID_AUTH_TEMPLATE, INVALID_WABA; 409 TEMPLATE_EXISTS (mismo nombre e idioma ya activos; una plantilla rechazada del mismo nombre se sustituye).
curl -X POST "https://api.envoisms.ma/v1/templates" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_shipped",
"channel": "whatsapp",
"category": "utility",
"language": "fr",
"body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
"sample_values": [
"Karim",
"#1042"
]
}'{
"id": "tpl_8e2a4c6b1d3f5a7c9e0b2d4f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "catalogue_automne",
"channel": "whatsapp",
"body": "Découvrez notre sélection d’automne, {{1}} !",
"variables": "{\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[],\"carousel\":{\"header_type\":\"image\",\"cards\":[{\"header_example_url\":\"https://example.ma/veste.jpg\",\"body\":\"Veste en laine\",\"buttons\":[{\"type\":\"url\",\"text\":\"Voir\",\"url\":\"https://example.ma/veste\"}]},{\"header_example_url\":\"https://example.ma/pull.jpg\",\"body\":\"Pull en coton\",\"buttons\":[{\"type\":\"url\",\"text\":\"Voir\",\"url\":\"https://example.ma/pull\"}]}]},\"variables\":[\"1\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\"}}",
"category": "marketing",
"active": 1,
"created_at": "2026-10-22 09:20:41",
"status": "pending_meta",
"rejection_reason": null,
"meta_template_id": "1203948576102944",
"language": "fr",
"quality_score": null,
"quality_updated_at": null,
"waba_id": null
}Consultar una plantilla
Devuelve una plantilla activa de la cuenta, con su estado y, si fue rechazada, rejection_reason. Si no, 404 NOT_FOUND.
- Permiso de clave API requerido: send.
curl -X GET "https://api.envoisms.ma/v1/templates/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "order_shipped",
"channel": "whatsapp",
"body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
"variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
"category": "utility",
"active": 1,
"created_at": "2026-10-20 14:02:11",
"status": "approved",
"rejection_reason": null,
"meta_template_id": "1203948576102938",
"language": "fr",
"quality_score": "GREEN",
"quality_updated_at": "2026-10-21 08:00:00",
"waba_id": null
}Modificar una plantilla
Modifica una plantilla con los mismos campos que la creación; solo cambian los campos enviados. Responde con la fila actualizada.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| name | body | category | language | sample_values | header_* | footer | buttons | carousel | mixed | Opcional | Mismos campos y reglas que POST /v1/templates. Una plantilla SMS solo acepta name, body, category y variables. |
- Permiso de clave API requerido: send.
- WhatsApp, plantilla aún no enviada a Meta: la modificación la devuelve a revisión (pending_admin y luego los controles automáticos). Plantilla ya en Meta: la modificación se envía a Meta y la devuelve a pending_meta; el nombre y el idioma ya no cambian (400 TEMPLATE_IMMUTABLE_FIELD), ni la categoría de una plantilla aprobada; ninguna modificación durante pending_meta, in_appeal o disabled (409 TEMPLATE_EDIT_NOT_ALLOWED).
curl -X PATCH "https://api.envoisms.ma/v1/templates/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "order_shipped",
"channel": "whatsapp",
"body": "Bonjour {{1}}, votre commande {{2}} est en route.",
"variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
"category": "utility",
"active": 1,
"created_at": "2026-10-20 14:02:11",
"status": "pending_meta",
"rejection_reason": null,
"meta_template_id": "1203948576102938",
"language": "fr",
"quality_score": "GREEN",
"quality_updated_at": "2026-10-21 08:00:00",
"waba_id": null
}Eliminar una plantilla
Desactiva la plantilla. Una plantilla de WhatsApp ya en Meta también se elimina allí; meta_deleted indica si Meta lo hizo y meta_note por qué no.
- Permiso de clave API requerido: send.
curl -X DELETE "https://api.envoisms.ma/v1/templates/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"meta_deleted": true
}Enviar una plantilla a Meta
Envía a Meta una plantilla de WhatsApp que ya pasó la revisión de EnvoiSMS pero no está en Meta (Meta la eliminó, por ejemplo). No es una forma de saltarse la revisión.
- Permiso de clave API requerido: send.
- Rechazos: 409 TEMPLATE_PENDING_REVIEW (la plantilla espera la revisión de EnvoiSMS; irá a Meta una vez aprobada), 409 TEMPLATE_REJECTED_BY_REVIEW (rechazada en la revisión; modifíquela para devolverla a revisión), 409 TEMPLATE_ALREADY_ON_META, 400 INVALID_CHANNEL (no es una plantilla de WhatsApp).
curl -X POST "https://api.envoisms.ma/v1/templates/{id}/submit-meta" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"ok": true,
"template": {
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "order_shipped",
"channel": "whatsapp",
"body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
"variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
"category": "utility",
"active": 1,
"created_at": "2026-10-20 14:02:11",
"status": "pending_meta",
"rejection_reason": null,
"meta_template_id": "1203948576102951",
"language": "fr",
"quality_score": null,
"quality_updated_at": null,
"waba_id": null
},
"meta": { "id": "1203948576102951", "status": "PENDING", "category": "UTILITY" }
}Sincronizar plantillas desde Meta
Importa las plantillas de sus cuentas de WhatsApp Business (estado, categoría, calidad). Una plantilla que Meta ya no tiene pasa a deleted_on_meta, salvo si la sincronización está incompleta (complete: false).
- Permiso de clave API requerido: send.
curl -X POST "https://api.envoisms.ma/v1/templates/sync" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"ok": true,
"wabas": ["104857392017465"],
"fetched": 12,
"updated": 9,
"inserted": 3,
"deduped": 0,
"marked_deleted": 1,
"complete": true,
"errors": []
}Listar Sender IDs
Devuelve sus solicitudes de Sender ID SMS y su estado, junto con un nombre sugerido a partir de su perfil.
- Permiso de clave API requerido: sender_id.
curl -X GET "https://api.envoisms.ma/v1/sender-ids" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "sid_6a2c8e4b0d1f3a5c7e9b2d4f",
"sender_id": "MABOUTIQUE",
"status": "approved",
"rc_url": "/v1/sender-ids/rc/acct_7b2e4c6a8d0f1e3b5c7a9d2f/rc_1f3a5c7e9b2d4f6a8c0e2b4d.pdf",
"note": null,
"requested_at": "2026-10-02 10:15:00",
"reviewed_at": "2026-10-09 16:40:00",
"company_website": "https://maboutique.ma",
"sample_content": "Votre commande 4512 est expédiée.",
"message_type": "transactional",
"estimated_volume": "5000/mois",
"operator_emailed_at": "2026-10-02 10:20:00"
}
],
"suggestedSenderId": "MABOUTIQUE"
}Solicitar un Sender ID
Presenta una solicitud de Sender ID SMS acompañada del registro mercantil en PDF. Cuerpo multipart/form-data. Responde 201 (200 si se actualiza una solicitud existente) con el estado pending.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| sender_id | string | Requerido | Nombre solicitado: letras sin acento, dígitos y espacios, 11 caracteres como máximo, al menos una letra. Se rechaza un encabezado genérico (INFO, ALERT…) o el nombre de una institución protegida. |
| file | file | Requerido | Registro mercantil, PDF de 5 MB como máximo. |
| message_type | string | Opcional | transactional (por defecto) o promotional. |
| company_website | sample_content | estimated_volume | string | Opcional | Sitio web de la empresa, mensaje de ejemplo y volumen estimado, transmitidos a los operadores con la solicitud. |
- Permiso de clave API requerido: sender_id.
- Errores: 400 INVALID_FORM_DATA, MISSING_FIELD, INVALID_FIELD, SENDER_ID_GENERIC, INVALID_FILE_TYPE, FILE_TOO_LARGE; 403 SENDER_ID_PROTECTED_BRAND; 409 SENDER_ID_PENDING o SENDER_ID_APPROVED.
curl -X POST "https://api.envoisms.ma/v1/sender-ids" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "sid_6a2c8e4b0d1f3a5c7e9b2d4f",
"sender_id": "MABOUTIQUE",
"status": "pending",
"rc_url": "/v1/sender-ids/rc/acct_7b2e4c6a8d0f1e3b5c7a9d2f/rc_1f3a5c7e9b2d4f6a8c0e2b4d.pdf"
}Conexión de WhatsApp Business
Devuelve el estado de sus números de WhatsApp Business conectados y la lista de control del número predeterminado (readiness): lo que, según Meta, le impide enviar. Solo lectura; no se devuelve ningún secreto.
- Cualquier clave API válida: no se exige ningún permiso concreto.
- readiness: can_send vale AVAILABLE, LIMITED, BLOCKED o unknown (Meta no respondió); blockers enumera los motivos de Meta (entity, reason, resolution); payment_method vale missing cuando Meta indica que falta un medio de pago, unchecked en otro caso; business_verification es el estado de verificación de la empresa en Meta. readiness vale null sin número conectado. Lo que Meta no dijo se devuelve null, nunca como un valor sano por defecto.
- Conectar, desconectar o reconfigurar un número, y toda escritura bajo /v1/whatsapp (perfil, ajustes, herramientas), se hace desde una sesión del panel: una clave API recibe 403 FORBIDDEN. Solo POST /v1/whatsapp/lookup-phone y POST /v1/whatsapp/media/upload siguen abiertos a las claves.
curl -X GET "https://api.envoisms.ma/v1/whatsapp/waba-connection" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"readiness": {
"waba_id": "104857392017465",
"payment_method": "unchecked",
"can_send": "AVAILABLE",
"blockers": [],
"business_verification": "verified",
"approved_templates": 4,
"checked_at": "2026-10-22T08:00:12.000Z"
},
"connected": true,
"mode": "custom_waba",
"gateway_number": "+212 708-789095",
"waba_id": "104857392017465",
"phone_number_id": "109283746501928",
"phone_number": "+212522000000",
"name": "Ma Boutique",
"status": "connected",
"error": null,
"quality_rating": "GREEN",
"messaging_limit_tier": "TIER_1K",
"connected_at": "2026-09-30 11:02:44",
"meta_app_id": "1029384756102938",
"meta_config_id": "2039485761029384",
"preverified_phone_ids": [],
"connections": [
{
"id": "5f1c9e3a7b2d4f6a8c0e1b3d5f7a9c2e",
"waba_id": "104857392017465",
"phone_number_id": "109283746501928",
"phone_number": "+212522000000",
"verified_name": "Ma Boutique",
"status": "connected",
"is_default": true,
"webhook_url": null,
"settings": {},
"created_at": "2026-09-30 11:02:44",
"updated_at": "2026-10-20 08:15:02"
}
]
}Perfil de WhatsApp Business
Devuelve el perfil público de uno de sus números tal como lo tiene Meta: acerca de, dirección, descripción, correo, foto, sitios web, sector.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| phone_number_id | string | Opcional | Número conectado que se consulta; por defecto su número predeterminado. |
- Cualquier clave API válida: no se exige ningún permiso concreto.
- El perfil se actualiza desde el panel; con una clave API, POST /v1/whatsapp/profile responde 403 FORBIDDEN.
curl -X GET "https://api.envoisms.ma/v1/whatsapp/profile" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"ok": true,
"phone_number_id": "109283746501928",
"profile": {
"about": "Service client",
"address": "Casablanca, Maroc",
"description": "Prêt-à-porter, livraison partout au Maroc.",
"email": "contact@maboutique.ma",
"profile_picture_url": "https://pps.whatsapp.net/v/t61/...",
"websites": ["https://maboutique.ma"],
"vertical": "APPAREL"
}
}Enviar un código de verificación
Genera un código de un solo uso, lo envía y abre una sesión de verificación. No guarda ningún código: valide lo que escribe el usuario con POST /v1/verify/check. Responde 202.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| to | string | Requerido | Destinatario en formato E.164. |
| channel | string | Opcional | "sms" (por defecto) o "whatsapp". En WhatsApp el código sale como plantilla de autenticación — desde su propio número si ha conectado uno con una plantilla de autenticación aprobada, si no desde el número de EnvoiSMS — y pasa a SMS si Meta rechaza el envío. |
| app_id | string | Opcional | Aplicación Verify (vra_…) creada en el panel: fija longitud, validez, intentos, canales y cascada, y sustituye a code_length y expiry. |
| code_length | integer | Opcional | Longitud del código, de 4 a 8 dígitos. Por defecto: 6. |
| expiry | integer | Opcional | Validez del código en segundos, de 60 a 1800. Por defecto: 600. En WhatsApp, limitada a 600. |
| brand | string | Opcional | (SMS) Nombre mostrado en el mensaje, 32 caracteres como máximo. Por defecto: EnvoiSMS. Un nombre genérico se sustituye por ENVOISMS; se rechaza el nombre de una institución protegida (403 SENDER_ID_PROTECTED_BRAND). |
| template | string | Opcional | (SMS) Texto personalizado con {{code}} y {{brand}}. |
| otp_button_text | string | Opcional | (WhatsApp) Etiqueta del botón de copia, 25 caracteres como máximo. |
| otp_type | package_name | signature_hash | string | Opcional | (WhatsApp) copy_code (por defecto), one_tap, zero_tap o url; para one_tap y zero_tap, el nombre del paquete Android y el hash de firma de la aplicación que recibe el código. |
| web_otp_domain | app_hash | string | Opcional | (SMS) Dominio para el autocompletado WebOTP (añade «@dominio #code» al SMS); hash de 11 caracteres de la aplicación Android para la API SMS Retriever. |
| auto_cascade | boolean | Opcional | Envía también el SMS de respaldo cuando el plazo del primer canal expira sin entrega. Sustituye el ajuste de la aplicación Verify. |
- Permiso de clave API requerido: verify.
- En WhatsApp la respuesta lleva "channel": "whatsapp", "resend_after_seconds": 60, "fallback_channels": ["sms", "whatsapp"] y "cost" ({eur, mad}), sin message_id. Con una clave sandbox no se envía ni se cobra nada y la respuesta añade "sandbox": true, "simulated": true y "sandbox_code", el código que se envía a /v1/verify/check.
- Límites por número: un envío cada 30 s por SMS y 60 s por WhatsApp (429 OTP_COOLDOWN, con Retry-After), 5 por hora y 10 por día (429 OTP_RATE_LIMIT); 200 envíos por día y por cuenta.
- Autocompletado: en su sitio, añada autocomplete="one-time-code" e inputmode="numeric" al campo del código.
curl -X POST "https://api.envoisms.ma/v1/verify/send" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"brand": "MyApp"
}'{
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"message_id": "msg_2b4d6f8a0c1e3b5d7f9a1c3e",
"to": "+212612345678",
"channel": "sms",
"expires_at": "2026-10-22T09:24:03.000Z",
"resend_after_seconds": 30,
"fallback_channels": [],
"status": "sent"
}Reenviar el código
Reenvía el código de una sesión aún abierta, por SMS o WhatsApp, una vez pasado el tiempo de espera. El código no cambia. Responde 200.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| session_id | string | Requerido | Sesión devuelta por /v1/verify/send (vrf_…). |
| channel | string | Opcional | "sms" (por defecto) o "whatsapp". |
- Permiso de clave API requerido: verify.
- El tiempo de espera corre desde el último envío real: 30 s tras un SMS, 60 s tras un WhatsApp. GET /v1/verify/:id da la cuenta atrás (resend_available_in_seconds) y el 429 OTP_COOLDOWN lleva Retry-After. La ventana es propia de su cuenta y del número.
- Errores: 404 SESSION_NOT_FOUND (desconocida, expirada o del otro modo de clave), 400 SESSION_EXPIRED, 400 INVALID_STATE (sesión ya verificada o cerrada), 400 UNSUPPORTED_CHANNEL, 429 OTP_COOLDOWN. Con una clave sandbox la respuesta añade "sandbox": true, "simulated": true y "sandbox_code".
curl -X POST "https://api.envoisms.ma/v1/verify/resend" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "vrf_…",
"channel": "sms"
}'{
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"message_id": "msg_8c0e2b4d6f1a3c5e7b9d1f3a",
"to": "+212612345678",
"channel": "sms",
"status": "sent",
"resend_after_seconds": 30,
"expires_at": "2026-10-22T09:24:03.000Z"
}Comprobar un código
Compara el código que escribió el usuario con el de la sesión. Un código correcto cierra la sesión: una segunda comprobación responde 404.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| session_id | string | Requerido | Sesión devuelta por /v1/verify/send. |
| code | string | Requerido | El código que escribió el usuario. |
- Permiso de clave API requerido: status.
- Errores: 400 INVALID_CODE (el mensaje indica los intentos restantes), 429 MAX_ATTEMPTS (la sesión pasa a failed), 404 SESSION_NOT_FOUND — también para una sesión expirada o ya verificada. /v1/verify nunca devuelve EXPIRED_CODE.
curl -X POST "https://api.envoisms.ma/v1/verify/check" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "vrf_…",
"code": "483921"
}'{
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"verified": true,
"verified_at": "2026-10-22T09:15:12.408Z"
}Anular una sesión
Cierra una sesión pendiente: el código enviado deja de aceptarse. También disponible como POST /v1/verify/:id/void.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| session_id | string | Requerido | Sesión que se anula (omítala si el identificador va en la ruta). |
- Permiso de clave API requerido: status.
- Errores: 404 SESSION_NOT_FOUND, 409 INVALID_STATE (sesión ya verificada, fallida o expirada). Si la aplicación Verify tiene callback_url, se le notifica (expired).
curl -X POST "https://api.envoisms.ma/v1/verify/void" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "vrf_…"
}'{
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"status": "expired",
"voided": true
}Consultar una sesión
Devuelve el estado de una sesión de verificación: estado, intentos, cuenta atrás antes del reenvío y resultado de entrega de cada envío.
- Permiso de clave API requerido: status.
- status vale pending, verified, failed o expired. 404 NOT_FOUND si la sesión no pertenece a la cuenta.
curl -X GET "https://api.envoisms.ma/v1/verify/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"to": "+212612345678",
"channel": "sms",
"status": "pending",
"attempts_used": 1,
"attempts_remaining": 2,
"max_attempts": 3,
"resend_available_in_seconds": 0,
"can_resend_now": true,
"expires_at": "2026-10-22T09:24:03.000Z",
"verified_at": null,
"created_at": "2026-10-22 09:14:03",
"app_id": null,
"app_name": null,
"attempted_channels": [],
"deliveries": [
{
"channel": "sms",
"kind": "send",
"status": "delivered",
"simulated": false,
"submitted_at": "2026-10-22 09:14:03",
"delivered_at": "2026-10-22 09:14:07",
"read_at": null,
"failed_at": null,
"error_code": null,
"error_message": null
}
],
"callback_delivered": null,
"cascade_triggered": false
}Identificar el operador de un número
Valida un número y devuelve su país y, para un móvil marroquí, el operador que lo atiende, portabilidad incluida. Se factura cada consulta correcta; un número inválido (400) no se factura.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| number | string | Opcional | Alternativa al parámetro de ruta: GET /v1/lookup?number=%2B212612345678. |
- Permiso de clave API requerido: send.
- Errores: 400 MISSING_FIELD, 400 INVALID_PHONE, 402 INSUFFICIENT_BALANCE. Con una clave sandbox la respuesta es real pero no se cobra nada, y lleva "sandbox": true. type vale mobile o unknown; operator vale null si se desconoce.
curl -X GET "https://api.envoisms.ma/v1/lookup/{number}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "lkp_5d7f9b1c3e5a7c9e1b3d5f7a",
"number": "+212612345678",
"valid": true,
"country_iso": "MA",
"type": "mobile",
"operator": "Maroc Telecom",
"ported": false,
"cost": { "eur": 0.005, "mad": 0.055 }
}Consultar la red (HLR)
Consulta el registro del operador (HLR): ¿el número es localizable ahora, qué red lo atiende hoy (portabilidad incluida), está en itinerancia, es móvil o fijo? Se factura cada respuesta de la red, sea cual sea; un número inválido (400) o una consulta no completada (502/503) no se factura.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| number | string | Opcional | Alternativa al parámetro de ruta: GET /v1/hlr?number=%2B212612345678. |
- Permiso de clave API requerido: send.
- status vale active (localizable), absent (conocido pero apagado o sin cobertura), inactive (fuera de servicio), invalid o unknown. La respuesta tarda un viaje de ida y vuelta a la red (1 a 5 s). source indica de dónde viene la respuesta: network = el registro del operador consultado en directo (status refleja la localizabilidad); portability = la base de portabilidad de los operadores (números marroquíes): operador, portabilidad y línea en servicio (active), sin decir si el teléfono está encendido en ese instante. Errores: 400 MISSING_FIELD, 400 INVALID_PHONE, 402 INSUFFICIENT_BALANCE, 502 BAD_GATEWAY, 503 HLR_UNAVAILABLE. Con una clave sandbox la respuesta se simula a partir de lo que la plataforma ya sabe, no se consulta ni se cobra nada, y lleva "sandbox": true.
curl -X GET "https://api.envoisms.ma/v1/hlr/{number}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "hlr_5d7f9b1c3e5a7c9e1b3d5f7a",
"number": "+212612345678",
"valid": true,
"status": "active",
"type": "mobile",
"country_iso": "MA",
"operator": "Maroc Telecom",
"mcc": "604",
"mnc": "01",
"ported": false,
"ported_operator": null,
"ported_mnc": null,
"roaming": false,
"roaming_country_iso": null,
"roaming_operator": null,
"roaming_mnc": null,
"source": "portability",
"cost": { "eur": 0.018, "mad": 0.20 }
}Validar el formato de un número
Valida el formato de un número y devuelve sus formas local e internacional, país, ubicación, operador de asignación y tipo de línea. No se factura.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| number | string | Requerido | Número, en formato local o internacional. |
| country_code | string | Opcional | Código de país ISO de 2 letras (MA, FR…), recomendado para un número en formato local. |
- Permiso de clave API requerido: status.
- carrier es el operador al que se asignó el rango, no necesariamente el que atiende un número portado: para el operador real, use GET /v1/lookup/:number. Errores: 400 MISSING_FIELD; 503 LOOKUP_UNAVAILABLE cuando el servicio no está disponible momentáneamente.
curl -X POST "https://api.envoisms.ma/v1/verify/lookup" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"number": "+212612345678"
}'{
"valid": true,
"number": "212612345678",
"local_format": "0612345678",
"international_format": "+212612345678",
"country_prefix": "+212",
"country_code": "MA",
"country_name": "Morocco",
"location": null,
"carrier": "Maroc Telecom",
"line_type": "mobile"
}Listar conversaciones
Devuelve los hilos de WhatsApp y SMS de la cuenta, con el estado de la ventana de 24 horas de cada uno y contadores globales.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| limit | integer | Opcional | Número de resultados: 50 por defecto, 100 como máximo. |
| offset | integer | Opcional | Número de resultados que se omiten (paginación). Por defecto: 0. |
| filter | string | Opcional | all (por defecto), unread, active_window o needs_human. |
| channel | string | Opcional | all (por defecto), whatsapp o sms. |
| phone_number_id | assigned_to | search | string | Opcional | Número de WhatsApp conectado; agente asignado; búsqueda en el número, el nombre y el último mensaje. |
- Permiso de clave API requerido: send.
curl -X GET "https://api.envoisms.ma/v1/conversations?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "th_212612345678",
"phone": "212612345678",
"contact_name": "Yassine Alami",
"channel": "whatsapp",
"lead_status": "new",
"unread_count": 1,
"bot_muted": false,
"handling_agent": null,
"notes": null,
"tags": [],
"last_inbound_at": "2026-10-22 08:42:00",
"last_outbound_at": "2026-10-21 17:10:00",
"last_message_preview": "Bonjour, la veste existe en taille L ?",
"last_message_role": "customer",
"last_message_at": "2026-10-22 08:42:00",
"is_window_active": true,
"window_expires_at": "2026-10-23T08:42:00.000Z",
"window_seconds_remaining": 84960,
"waba_name": "Ma Boutique",
"waba_phone": "+212522000000",
"phone_number_id": "109283746501928"
}
],
"total": 1,
"limit": 50,
"offset": 0,
"stats": {
"total_threads": 1,
"unread_threads": 1,
"active_window_threads": 1,
"human_takeover_threads": 0
}
}Consultar una conversación
Devuelve un hilo y sus mensajes (500 como máximo), del más antiguo al más reciente, con el estado de la ventana de 24 horas.
- Permiso de clave API requerido: send.
- :phone acepta el número con o sin +. role vale customer para un mensaje recibido, agent o bot para uno enviado.
curl -X GET "https://api.envoisms.ma/v1/conversations/{phone}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"thread": {
"id": "th_212612345678",
"phone": "212612345678",
"contact_name": "Yassine Alami",
"channel": "whatsapp",
"lead_status": "new",
"unread_count": 0,
"bot_muted": true,
"handling_agent": "Salma",
"notes": null,
"tags": [],
"last_inbound_at": "2026-10-22 08:42:00",
"last_outbound_at": "2026-10-22 08:45:10",
"is_window_active": true,
"window_expires_at": "2026-10-23T08:42:00.000Z",
"window_seconds_remaining": 84960,
"window": {
"is_window_active": true,
"window_expires_at": "2026-10-23T08:42:00.000Z",
"window_seconds_remaining": 84960
},
"waba_name": "Ma Boutique",
"waba_phone": "+212522000000",
"phone_number_id": "109283746501928"
},
"messages": [
{
"id": "wamsg_1c3e5a7c9e1b3d5f7a9c1e3b",
"phone": "212612345678",
"role": "customer",
"message": "Bonjour, la veste existe en taille L ?",
"wamid": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg",
"status": "read",
"media_type": "text",
"media_url": null,
"metadata": {},
"read_at": "2026-10-22 08:43:00",
"created_at": "2026-10-22 08:42:00"
}
]
}Responder en una conversación
Envía una respuesta de agente (texto, medio o plantilla) y pausa el bot en ese hilo. El envío pasa por el mismo circuito que POST /v1/messages: se aplican la tarificación, la lista de bajas y el cobro. Responde 201.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| message | string | Opcional | Texto de la respuesta, o pie del medio. Obligatorio sin plantilla ni medio (400 MISSING_MESSAGE). |
| channel | string | Opcional | "whatsapp" (por defecto) o "sms". |
| template_id | template_name | template_language | variables | mixed | Opcional | Plantilla de WhatsApp aprobada que se envía en lugar de texto libre (por identificador, o por nombre e idioma), con sus valores (array u objeto). |
| media_url | media_type | filename | string | Opcional | Medio adjunto: URL https (o data URI), tipo image o document, nombre de archivo del documento. |
| phone_number_id | string | Opcional | Número de WhatsApp conectado que responde. |
- Permiso de clave API requerido: send.
- Un mensaje libre de WhatsApp exige que el contacto haya escrito en las últimas 24 horas (si no, 400 OUT_OF_24H_WINDOW); una plantilla aprobada sale en cualquier momento. Otros errores: 403 WHATSAPP_NOT_CONNECTED, 404 TEMPLATE_NOT_FOUND, 400 TEMPLATE_NOT_APPROVED.
curl -X POST "https://api.envoisms.ma/v1/conversations/{phone}/messages" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Thanks, we will call you back today."
}'{
"ok": true,
"message": {
"id": "msg_9e1b3d5f7a9c1e3b5d7f9a1c",
"phone": "212612345678",
"role": "agent",
"message": "Oui, la taille L est disponible.",
"wamid": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAERgS",
"status": "sent",
"channel": "whatsapp",
"media_type": "text",
"metadata": { "agent": "Salma" },
"created_at": "2026-10-22T08:45:10.221Z"
}
}Listar leads calificados
Devuelve los leads que registró su bot de WhatsApp, del más reciente al más antiguo, con su puntuación (0 a 100) y su estado. format=csv descarga la misma lista.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| limit | integer | Opcional | Número de resultados: 100 por defecto, 500 como máximo. |
| status | string | Opcional | new, contacted, qualified, Fournisseur, won o lost. |
| preset | string | Opcional | Sector del bot (por ejemplo medical_equipment); supplier agrupa las solicitudes de proveedores. |
| search | string | Opcional | Búsqueda en el número, el nombre, la categoría y las notas. |
| format | string | Opcional | json (por defecto) o csv. |
- Cualquier clave API válida: no se exige ningún permiso concreto.
- Sin paginación por offset: limit acota la lista. metrics se calcula sobre los leads devueltos; hot cuenta los que alcanzan 80.
curl -X GET "https://api.envoisms.ma/v1/qualified-leads?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"ok": true,
"leads": [
{
"id": "lead_3a5c7e9b1d3f5a7c9e1b3d5f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"phone_number_id": "109283746501928",
"contact_phone": "+212612345678",
"contact_name": "Cabinet Benjelloun",
"preset": "medical_equipment",
"category": "Échographe",
"bant_score": 85,
"urgency": "immediate",
"budget": "200 000 - 300 000 MAD",
"authority": "decision_maker",
"notes": null,
"source_type": "direct_inbound",
"status": "new",
"created_at": "2026-10-21 09:15:00",
"updated_at": "2026-10-21 09:15:00"
}
],
"metrics": { "total": 1, "hot": 1, "average_score": 85 }
}Actualizar un lead
Cambia el estado, las notas o el nombre de un lead. Responde con el lead actualizado.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| status | string | Opcional | new, contacted, qualified, Fournisseur, won o lost (si no, 400 INVALID_STATUS). |
| notes | string | Opcional | Notas internas de seguimiento. |
| contact_name | string | Opcional | Nombre del contacto o del establecimiento. |
| value_minor | currency | integer | string | Opcional | Valor del negocio en céntimos y su divisa, transmitidos a Meta con un paso a won. |
- Cualquier clave API válida: no se exige ningún permiso concreto.
- El paso a qualified o won se comunica a Meta (QualifiedLead, Purchase) cuando el lead llegó desde un anuncio que lleva a WhatsApp; solo ante un cambio real de estado. 404 LEAD_NOT_FOUND si el lead no pertenece a la cuenta.
curl -X PATCH "https://api.envoisms.ma/v1/qualified-leads/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "contacted",
"notes": "Called back on 22/10"
}'{
"ok": true,
"lead": {
"id": "lead_3a5c7e9b1d3f5a7c9e1b3d5f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"phone_number_id": "109283746501928",
"contact_phone": "+212612345678",
"contact_name": "Cabinet Benjelloun",
"preset": "medical_equipment",
"category": "Échographe",
"bant_score": 85,
"urgency": "immediate",
"budget": "200 000 - 300 000 MAD",
"authority": "decision_maker",
"notes": "Rendez-vous fixé jeudi.",
"source_type": "direct_inbound",
"status": "contacted",
"created_at": "2026-10-21 09:15:00",
"updated_at": "2026-10-22 10:02:31",
"user_id": null
}
}Listar contactos
Devuelve los contactos de la cuenta, con búsqueda y filtro por lista.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| limit | integer | Opcional | Número de resultados: 100 por defecto, 500 como máximo. |
| offset | integer | Opcional | Número de resultados que se omiten (paginación). Por defecto: 0. |
| q | string | Opcional | Búsqueda en nombre, teléfono y email. |
| list_id | string | Opcional | Solo los miembros de esta lista (lst_…). |
- Permiso de clave API requerido: contacts.
curl -X GET "https://api.envoisms.ma/v1/contacts?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "ctc_2d4f6a8c0e1b3d5f7a9c1e3b",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"phone": "+212612345678",
"name": "Karim Bennani",
"email": "karim@example.ma",
"custom1": "VIP",
"custom2": null,
"custom3": null,
"opted_out": 0,
"opted_out_at": null,
"created_at": "2026-10-10 14:20:00"
}
],
"limit": 100,
"offset": 0,
"total": 1
}Crear o actualizar un contacto
Crea un contacto, o actualiza el que ya tiene ese número (se sustituyen nombre, email y campos personalizados). Responde 201 con el contacto.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| phone | string | Requerido | Número en formato E.164 (si no, 400 INVALID_PHONE). |
| name | email | string | Opcional | Nombre y email del contacto. |
| list_id | string | Opcional | Añade también el contacto a esta lista (404 LIST_NOT_FOUND si no es suya). |
| custom_fields | object | Opcional | {"custom1", "custom2", "custom3"}: tres campos libres, utilizables en las campañas. |
- Permiso de clave API requerido: contacts.
curl -X POST "https://api.envoisms.ma/v1/contacts" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+212612345678",
"name": "Karim Bennani",
"email": "karim@example.ma"
}'{
"id": "ctc_2d4f6a8c0e1b3d5f7a9c1e3b",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"phone": "+212612345678",
"name": "Karim Bennani",
"email": "karim@example.ma",
"custom1": "VIP",
"custom2": null,
"custom3": null,
"opted_out": 0,
"opted_out_at": null,
"created_at": "2026-10-10 14:20:00"
}Importar contactos
Pone en cola una importación de contactos, en JSON o en archivo CSV, y responde 202 de inmediato; la importación se ejecuta después por lotes de 100.
- Permiso de clave API requerido: contacts.
- La respuesta se da al poner en cola: total cuenta las filas recibidas e imported lo repite tal cual. Los números inválidos se descartan durante la importación; compruebe el resultado con GET /v1/contacts. Otros tipos de contenido: 415 UNSUPPORTED_MEDIA_TYPE.
curl -X POST "https://api.envoisms.ma/v1/contacts/import" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{
"phone": "+212611111111",
"name": "Karim"
},
{
"phone": "+212622222222",
"name": "Salma"
}
]
}'{
"jobId": "job_4b6d8f0a2c4e6b8d0f2a4c6e",
"total": 150,
"imported": 150,
"skipped": 0,
"status": "queued"
}Eliminar un contacto
Elimina definitivamente un contacto. 404 NOT_FOUND si no existe.
- Permiso de clave API requerido: contacts.
curl -X DELETE "https://api.envoisms.ma/v1/contacts/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "ctc_2d4f6a8c0e1b3d5f7a9c1e3b"
}Listar las listas de contactos
Devuelve las listas de contactos de la cuenta, con su número de miembros.
- Permiso de clave API requerido: contacts.
curl -X GET "https://api.envoisms.ma/v1/contacts/lists" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "Clients fidèles",
"description": "Clients ayant commandé au moins deux fois",
"count": 1420,
"created_at": "2026-09-15 09:00:00"
}
]
}Crear una lista
Crea una lista de contactos vacía, que se llena con POST /v1/contacts o /v1/contacts/import. Responde 201.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| name | string | Requerido | Nombre de la lista. |
| description | string | Opcional | Descripción libre. |
- Permiso de clave API requerido: contacts.
curl -X POST "https://api.envoisms.ma/v1/contacts/lists" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Newsletter",
"description": "Customers who subscribed at checkout"
}'{
"id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"name": "Clients fidèles",
"description": "Clients ayant commandé au moins deux fois",
"count": 0
}Renombrar una lista
Sustituye el nombre y la descripción de una lista. 404 NOT_FOUND si no existe.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| name | string | Requerido | Nuevo nombre. |
| description | string | Opcional | Nueva descripción; si se omite, se borra. |
- Permiso de clave API requerido: contacts.
curl -X POST "https://api.envoisms.ma/v1/contacts/lists/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"name": "Clients fidèles 2026",
"description": null
}Eliminar una lista
Elimina una lista y sus pertenencias; los contactos se conservan. Se rechaza (400 LIST_IN_USE) mientras una campaña programada, en curso o en pausa la use.
- Permiso de clave API requerido: contacts.
curl -X DELETE "https://api.envoisms.ma/v1/contacts/lists/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "lst_f84b2d6a8c0e1f3b5d7a9c2e"
}Listar las bajas
Devuelve los números dados de baja de sus envíos. No se les envía ningún mensaje (403 OPTED_OUT).
- Permiso de clave API requerido: optouts.
curl -X GET "https://api.envoisms.ma/v1/optouts?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{ "phone": "+212611111111", "opted_out_at": "2026-10-01 12:00:00" }
],
"limit": 100,
"offset": 0,
"total": 1
}Añadir una baja
Añade un número a su lista de bajas. Responde 201.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| phone | string | Requerido | Número en formato E.164. |
- Permiso de clave API requerido: optouts.
curl -X POST "https://api.envoisms.ma/v1/optouts" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+212612345678"
}'{
"phone": "+212611111111",
"opted_out": true
}Retirar una baja
Retira un número de la lista de bajas. Hágalo solo con un nuevo consentimiento de la persona, que debe registrarse con POST /v1/consents.
- Permiso de clave API requerido: optouts.
curl -X DELETE "https://api.envoisms.ma/v1/optouts/{phone}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"phone": "+212611111111",
"opted_out": false
}Listar consentimientos
Registro de consentimientos de solo adición (ley 09-08): cada consentimiento y cada retirada es un evento fechado, con su fuente y su prueba. La plataforma registra por sí misma las palabras clave STOP/START recibidas y la primera conversación que abre el cliente.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| limit | integer | Opcional | Número de resultados: 100 por defecto, 500 como máximo. |
| offset | integer | Opcional | Número de resultados que se omiten (paginación). Por defecto: 0. |
| phone | string | Opcional | Número E.164. |
| purpose | string | Opcional | marketing, transactional, otp o service. |
| status | string | Opcional | granted o withdrawn. |
| from | to | string | Opcional | Límites ISO 8601 sobre recorded_at. |
| format | string | Opcional | csv: descarga todo el conjunto filtrado. |
- Permiso de clave API requerido: contacts.
curl -X GET "https://api.envoisms.ma/v1/consents?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "cns_9f2a4c6e8b0d1f3a5c7e9b2d",
"phone": "+212612345678",
"channel": "whatsapp",
"purpose": "marketing",
"status": "withdrawn",
"source": "whatsapp_keyword",
"evidence": { "keyword": "stop", "lang": "fr" },
"recorded_at": "2026-10-05T09:00:00.000Z",
"created_at": "2026-10-05T09:00:00.000Z"
}
],
"limit": 100,
"offset": 0,
"total": 1
}Registrar un consentimiento
Registra que una persona dio o retiró su consentimiento, con la prueba que usted tiene. Una retirada de marketing pone el número en la lista de bajas de inmediato. Responde 201.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| phone | string | Requerido | Número en formato E.164. |
| purpose | string | Requerido | marketing, transactional, otp o service. |
| status | string | Opcional | granted (por defecto) o withdrawn. |
| channel | string | Opcional | whatsapp, sms o any (por defecto). |
| source | string | Opcional | api (por defecto), form, import o dashboard. Las fuentes por palabra clave están reservadas a la plataforma. |
| evidence | object | Opcional | Objeto JSON libre de menos de 4 096 bytes: texto mostrado, URL, IP, referencia. |
| recorded_at | string | Opcional | Fecha ISO 8601 del acto (por defecto: ahora); puede antedatarse para una importación, nunca en el futuro. |
- Permiso de clave API requerido: contacts.
- Errores: 400 INVALID_PHONE, INVALID_PURPOSE, INVALID_STATUS, INVALID_CHANNEL, INVALID_SOURCE, INVALID_EVIDENCE, INVALID_DATE.
curl -X POST "https://api.envoisms.ma/v1/consents" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+212612345678",
"purpose": "marketing",
"status": "granted",
"channel": "whatsapp",
"source": "form",
"evidence": {
"form": "checkout",
"ip": "196.200.1.4"
}
}'{
"id": "cns_1b3d5f7a9c2e4b6d8f0a2c4e",
"phone": "+212612345678",
"channel": "any",
"purpose": "marketing",
"status": "granted",
"source": "form",
"evidence": { "text": "J’accepte de recevoir les offres par WhatsApp", "url": "https://example.ma/inscription" },
"recorded_at": "2026-10-01T10:00:00.000Z",
"created_at": "2026-10-01T10:00:00.000Z"
}Estado e historial de un número
Devuelve el estado actual por finalidad (del evento más reciente; unknown sin historial, nunca presumido), la presencia en la lista de bajas y el historial (500 eventos como máximo).
- Permiso de clave API requerido: contacts.
curl -X GET "https://api.envoisms.ma/v1/consents/{phone}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"phone": "+212612345678",
"opted_out": true,
"opted_out_at": "2026-10-05T09:00:00.000Z",
"state": {
"marketing": { "status": "withdrawn", "channel": "whatsapp", "source": "whatsapp_keyword", "recorded_at": "2026-10-05T09:00:00.000Z", "record_id": "cns_9f2a4c6e8b0d1f3a5c7e9b2d" },
"transactional": { "status": "unknown" },
"otp": { "status": "unknown" },
"service": { "status": "granted", "channel": "whatsapp", "source": "whatsapp_inbound", "recorded_at": "2026-07-01T09:00:00.000Z", "record_id": "cns_0a2c4e6b8d1f3a5c7e9b0d2f" }
},
"history": []
}Listar campañas
Devuelve las campañas de la cuenta, de la más reciente a la más antigua, con sus contadores.
- Permiso de clave API requerido: campaigns.
- Estados: draft, scheduled, running, paused, completed, failed, cancelled.
curl -X GET "https://api.envoisms.ma/v1/campaigns?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "Soldes d’automne",
"channel": "sms",
"list_id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"template_id": null,
"body": "Bonjour {{name}}, -30% sur la collection avec le code AUT30.",
"sender_id": "MABOUTIQUE",
"status": "completed",
"scheduled_at": null,
"started_at": "2026-10-15 10:00:00",
"completed_at": "2026-10-15 10:06:12",
"total_count": 1420,
"sent_count": 1412,
"delivered_count": 1380,
"failed_count": 8,
"cost_eur": 102.24,
"cost_mad": 1124.64,
"created_at": "2026-10-14 16:30:00",
"buttons": null,
"template_variables": null
}
],
"total": 1,
"limit": 50,
"offset": 0
}Crear una campaña
Crea una campaña en borrador, o programada si se indica scheduled_at. No sale nada antes de POST /v1/campaigns/:id/send. Responde 201.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| name | string | Requerido | Nombre de la campaña. |
| body | string | Opcional | (SMS) Texto del mensaje. Se exige body o template_id. |
| template_id | string | Opcional | Plantilla de la cuenta. Obligatoria en WhatsApp, donde Meta debe haberla aprobado (400 TEMPLATE_NOT_APPROVED). |
| channel | string | Opcional | "sms" (por defecto) o "whatsapp" (exige un número de WhatsApp Business conectado, si no 403 WHATSAPP_NOT_CONNECTED). |
| list_id | string | Opcional | Lista destinataria; necesaria para lanzar la campaña (404 LIST_NOT_FOUND si no es suya). |
| sender_id | string | Opcional | SMS: Sender ID (mismas reglas que POST /v1/messages; por defecto EnvoiSMS). WhatsApp: número conectado que envía (por defecto, su número predeterminado; 400 INVALID_WABA_SENDER si no es suyo). |
| scheduled_at | string | Opcional | Fecha ISO 8601; pone la campaña en estado scheduled. |
| template_variables | object | Opcional | (WhatsApp) Valor de cada parámetro de la plantilla, por clave. Clave "1", "2"… o nombre de variable: un campo del contacto (name, phone, email, custom1, custom2, custom3) o un texto fijo con el prefijo "static:". "_header_media_url": enlace https del medio del encabezado (obligatorio para un encabezado de imagen, vídeo o documento). "_button_<i>": parte variable del botón de enlace de índice i (desde 0). Sin valor, {{1}} lee name, {{2}} custom1 y {{3}} custom2. |
| consent_attested | boolean | Opcional | (WhatsApp, plantilla marketing) true: usted declara que cada contacto de la lista aceptó recibir sus mensajes de WhatsApp. Obligatorio para una plantilla marketing (si no, 400 CONSENT_ATTESTATION_REQUIRED); la declaración queda fechada. |
| buttons | array | Opcional | (WhatsApp) Botones, mismo formato que POST /v1/messages. Rechazados en SMS (400 BUTTONS_NOT_SUPPORTED). |
- Permiso de clave API requerido: campaigns.
- WhatsApp: todo lo que la plantilla exige por contacto se comprueba al crear; lo que falta se nombra en 400 MISSING_TEMPLATE_PARAMS. Otros errores: 400 MISSING_TEMPLATE, 404 TEMPLATE_NOT_FOUND.
curl -X POST "https://api.envoisms.ma/v1/campaigns" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Summer sale",
"channel": "sms",
"body": "Summer sale: -20% until Sunday. Reply STOP to unsubscribe.",
"list_id": "lst_…"
}'{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"status": "draft"
}Consultar una campaña
Devuelve una campaña, su estado y sus contadores de envío. Si no, 404 NOT_FOUND.
- Permiso de clave API requerido: campaigns.
curl -X GET "https://api.envoisms.ma/v1/campaigns/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "Nouvelle collection",
"channel": "whatsapp",
"list_id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"template_id": "tpl_8e2a4c6b1d3f5a7c9e0b2d4f",
"body": null,
"sender_id": "+212522000000",
"status": "running",
"scheduled_at": null,
"started_at": "2026-10-22 10:00:00",
"completed_at": null,
"total_count": 1420,
"sent_count": 840,
"delivered_count": 790,
"failed_count": 3,
"cost_eur": 0,
"cost_mad": 0,
"created_at": "2026-10-21 18:12:40",
"buttons": null,
"template_variables": "{\"1\":\"name\"}"
}Modificar una campaña
Modifica una campaña en borrador o programada; solo cambian los campos enviados. Responde con la campaña actualizada.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| name | body | sender_id | scheduled_at | string | Opcional | Nuevo nombre, texto, remitente o fecha programada (ISO 8601). Un valor vacío deja el campo sin cambios. |
- Permiso de clave API requerido: campaigns.
- 409 CAMPAIGN_NOT_EDITABLE si la campaña no está en draft ni en scheduled; 404 NOT_FOUND si no existe.
curl -X PATCH "https://api.envoisms.ma/v1/campaigns/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduled_at": "2026-11-01T09:00:00Z"
}'{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "Soldes d’automne — relance",
"channel": "sms",
"list_id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"template_id": null,
"body": "Dernier jour : -30% avec le code AUT30.",
"sender_id": "MABOUTIQUE",
"status": "scheduled",
"scheduled_at": "2026-10-25T09:00:00Z",
"started_at": null,
"completed_at": null,
"total_count": 0,
"sent_count": 0,
"delivered_count": 0,
"failed_count": 0,
"cost_eur": 0,
"cost_mad": 0,
"created_at": "2026-10-22 11:05:00",
"buttons": null,
"template_variables": null
}Cancelar una campaña
Pasa la campaña a cancelled; sigue siendo consultable. Una campaña en curso debe pausarse antes (409 CAMPAIGN_RUNNING).
- Permiso de clave API requerido: campaigns.
curl -X DELETE "https://api.envoisms.ma/v1/campaigns/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f"
}Lanzar una campaña
Lanza la campaña a los miembros de su lista que no se han dado de baja. El envío se hace en segundo plano; sígalo con GET /v1/campaigns/:id. Responde 202.
- Permiso de clave API requerido: campaigns.
- Errores: 400 ALREADY_RUNNING, 400 MISSING_LIST, 400 EMPTY_LIST (ningún destinatario elegible), 400 MISSING_BODY, 404 NOT_FOUND. Cada mensaje se tarifica y se cobra como un envío individual.
curl -X POST "https://api.envoisms.ma/v1/campaigns/{id}/send" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"status": "running",
"total": 1420
}Pausar una campaña
Pausa una campaña en curso. 409 NOT_RUNNING si no lo está.
- Permiso de clave API requerido: campaigns.
curl -X POST "https://api.envoisms.ma/v1/campaigns/{id}/pause" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"paused": true,
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f"
}Reanudar una campaña
Reanuda una campaña en pausa donde se detuvo. 409 NOT_PAUSED si no está en pausa.
- Permiso de clave API requerido: campaigns.
curl -X POST "https://api.envoisms.ma/v1/campaigns/{id}/resume" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"resumed": true,
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f"
}Listar webhooks
Devuelve sus webhooks activos. El secreto solo aparece enmascarado (secret_hint); se muestra completo únicamente al crear y al rotar.
- Permiso de clave API requerido: webhooks.
curl -X GET "https://api.envoisms.ma/v1/webhooks" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"name": "Webhook Endpoint",
"target_type": "custom",
"format": "standard",
"url": "https://example.ma/hooks/envoisms",
"events": "[\"message.delivered\",\"message.failed\"]",
"active": 1,
"last_triggered_at": "2026-10-22 09:14:09",
"last_status": 200,
"created_at": "2026-10-01 10:00:00",
"secret_hint": "••••9c2e"
}
]
}Crear un webhook
Registra una URL HTTPS que recibirá los eventos elegidos. Responde 201 con el secreto de firma, mostrado solo esta vez.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| url | string | Requerido | URL HTTPS pública (si no, 400 INVALID_URL). |
| events | string[] | Opcional | message.sent, message.delivered, message.read, message.failed, message.undeliverable, message.fallback, message.inbound, message.reaction, contact.optout, contact.optin, lead.qualified; comodines "message.*" y "*". Por defecto: ["message.delivered", "message.failed"]. |
| secret | string | Opcional | Secreto de firma; se genera (sec_…) si se omite. |
| name | format | string | Opcional | Etiqueta; formato standard ({"event", "data", "timestamp"}) o flat (campos de data también en la raíz, para una hoja de cálculo). |
- Permiso de clave API requerido: webhooks.
- Firma: cada envío lleva X-EnvoiSMS-Signature-256: t=<hora unix>,v1=<HMAC-SHA256 hexadecimal de "t.cuerpo" con el secreto>, y X-EnvoiSMS-Event. Rechace una marca de tiempo de más de 5 minutos. Un fallo (5xx, 429 o red) se reintenta tras 60 s, 10 min y luego 1 h: 4 intentos en total.
- El número de webhooks activos depende del plan (Starter 2, Business 5, Pro 15, Enterprise 50); por encima, 403 LIMIT_EXCEEDED.
curl -X POST "https://api.envoisms.ma/v1/webhooks" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.ma/webhooks/envoisms",
"events": [
"message.delivered",
"message.failed",
"message.inbound"
]
}'{
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"name": "Webhook Endpoint",
"target_type": "custom",
"format": "standard",
"url": "https://example.ma/hooks/envoisms",
"events": ["message.delivered", "message.failed"],
"secret": "sec_3d5f7a9c1e3b5d7f9a1c9c2e",
"active": true
}Modificar un webhook
Modifica la URL, los eventos, el nombre, el formato o el estado activo de un webhook; solo cambian los campos enviados.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| url | events | name | format | mixed | Opcional | Mismas reglas que al crear. |
| active | boolean | Opcional | false detiene los envíos a este webhook. |
- Permiso de clave API requerido: webhooks.
curl -X PATCH "https://api.envoisms.ma/v1/webhooks/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"active": false
}'{
"updated": true,
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e"
}Eliminar un webhook
Desactiva el webhook: no se le envía ningún evento más y desaparece de la lista.
- Permiso de clave API requerido: webhooks.
curl -X DELETE "https://api.envoisms.ma/v1/webhooks/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e"
}Probar un webhook
Envía un evento de ejemplo, firmado como uno real, a la URL del webhook. Responde 202 con el ejemplo enviado.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| event | string | Opcional | Nombre del evento que se simula (por defecto: lead.qualified). Los datos de ejemplo son un mensaje recibido para message.inbound y un lead calificado en otro caso. |
- Permiso de clave API requerido: webhooks.
curl -X POST "https://api.envoisms.ma/v1/webhooks/{id}/test" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "message.delivered"
}'{
"queued": true,
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"event": "message.inbound",
"sample": {
"event": "message.inbound",
"data": {
"message_id": "msg_demo_k3x9f2ab",
"from": "+212612345678",
"to": "+212708789095",
"channel": "whatsapp",
"text": "Bonjour, je souhaite des informations sur vos tarifs et solutions.",
"contact_name": "Karim Bennani",
"timestamp": "2026-10-22T09:30:00.000Z"
},
"timestamp": "2026-10-22T09:30:00.000Z"
}
}Rotar el secreto de un webhook
Sustituye el secreto de firma y devuelve el nuevo, solo esta vez. Los envíos ya en cola siguen firmados con el anterior: acepte ambos durante unos minutos.
- Permiso de clave API requerido: webhooks.
curl -X POST "https://api.envoisms.ma/v1/webhooks/{id}/rotate-secret" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"secret": "sec_7a9c1e3b5d7f9a1c3e5b0d2f",
"rotated": true
}Registro de envíos de webhook
Devuelve los 100 últimos intentos de envío, del más reciente al más antiguo, con el cuerpo enviado y la respuesta de su servidor.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| webhook_id | string | Opcional | Solo los envíos de este webhook. |
- Permiso de clave API requerido: webhooks.
curl -X GET "https://api.envoisms.ma/v1/webhooks/logs" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "0b6f3c2e-8a4d-4f1b-9c7e-2d5a1f8e6b30",
"webhook_id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"event": "message.delivered",
"url": "https://example.ma/hooks/envoisms",
"payload": "{\"event\":\"message.delivered\",\"data\":{\"id\":\"msg_3f9a1c7e5b2d4a6f8e0c1b3d\",\"to\":\"+212612345678\",\"channel\":\"sms\",\"status\":\"delivered\"},\"timestamp\":\"2026-10-22T09:14:09.120Z\"}",
"status_code": 500,
"error_message": null,
"attempt": 1,
"delivered": 0,
"created_at": "2026-10-22 09:14:09"
}
],
"max_attempts": 4
}Repetir un envío de webhook
Reenvía un envío registrado — mismo evento, mismos datos, firma nueva — a la URL actual del webhook, con los reintentos habituales. Responde 202.
- Permiso de clave API requerido: webhooks.
- 404 NOT_FOUND si el registro es desconocido o su webhook se eliminó; 422 REPLAY_UNAVAILABLE si no se conservó el cuerpo enviado.
curl -X POST "https://api.envoisms.ma/v1/webhooks/logs/{log_id}/replay" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"queued": true,
"replay_of": "0b6f3c2e-8a4d-4f1b-9c7e-2d5a1f8e6b30",
"webhook_id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"event": "message.delivered"
}Listar webhooks entrantes
Devuelve sus webhooks entrantes: URL de captura que convierten una llamada de una herramienta externa (formulario, Zapier, Make…) en un mensaje enviado.
- Cualquier clave API válida: no se exige ningún permiso concreto.
curl -X GET "https://api.envoisms.ma/v1/inbound-webhooks" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "inwh_9a8b7c6d5e4f3a2b1c0d9e8f",
"name": "Formulaire de contact",
"channel": "whatsapp",
"template_id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"active": 1,
"created_at": "2026-10-12 14:00:00"
}
]
}Crear un webhook entrante
Crea una URL de captura, POST /v1/inbound-webhooks/catch/:id: cada llamada JSON que recibe envía un mensaje al número leído en el cuerpo. Responde 201.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| name | string | Requerido | Etiqueta de la fuente. |
| channel | string | Requerido | sms o whatsapp. |
| mapping_config | object | Requerido | {"to_path": "contact.phone", "variables": {"1": "contact.name"}}: ruta del número en el JSON recibido y ruta de cada variable de la plantilla. |
| template_id | string | Opcional | Plantilla que se envía (identificador o nombre). |
- Cualquier clave API válida: no se exige ningún permiso concreto.
- La URL de captura no pide clave: su identificador hace de secreto, no lo publique. Acepta 64 KB como máximo y 120 llamadas por minuto, responde 202 {"success": true, "messageId"} y cobra su saldo como un envío normal. Para eliminarla: DELETE /v1/inbound-webhooks/:id.
curl -X POST "https://api.envoisms.ma/v1/inbound-webhooks" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Website form",
"channel": "whatsapp",
"mapping_config": {
"to_path": "phone"
}
}'{
"id": "inwh_9a8b7c6d5e4f3a2b1c0d9e8f"
}Consultar el saldo
Devuelve el saldo prepago en dírhams y euros, el plan y su fecha de vencimiento.
- Permiso de clave API requerido: balance.
- GET /v1/balance devuelve los cuatro primeros campos con el mismo permiso.
curl -X GET "https://api.envoisms.ma/v1/billing/balance" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"balance_mad": 1492.5,
"balance_eur": 135.68,
"primary_currency": "MAD",
"plan": "business",
"plan_expires_at": "2026-12-31 23:59:59",
"plan_days_left": 70
}Listar transacciones
Devuelve los movimientos del saldo, del más reciente al más antiguo: recargas, consumo, reembolsos, ajustes y créditos regalados, cada uno con el saldo posterior.
- Permiso de clave API requerido: billing.
curl -X GET "https://api.envoisms.ma/v1/billing/transactions?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "tx_6e8a0c2e4b6d8f0a2c4e6b8d",
"type": "usage",
"status": "completed",
"amount_eur": -0.072,
"amount_mad": -0.792,
"balance_after_eur": 135.68,
"balance_after_mad": 1492.5,
"payment_method": null,
"description": "SMS +212612345678",
"reference": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
"stripe_invoice_id": null,
"created_at": "2026-10-22 09:14:03"
}
],
"limit": 50,
"offset": 0,
"total": 1,
"type": null
}Estado de los pagos máquina
Indica si un agente sin cuenta puede pagar el acceso a la API con un depósito en USDC (HTTP 402, protocolo MPP), y en qué condiciones. Sin autenticación.
- Si está desactivado, la respuesta es {"enabled": false}.
- Recorrido: llame a /v1/messages, /v1/waba, /v1/verify o /v1/mcp sin cabecera Authorization y con X-Payment-Method: mpp (o Accept: application/problem+json); la respuesta es 402 con una cabecera WWW-Authenticate: Payment que describe el depósito. Deposite deposit_usd en USDC y repita la llamada con Authorization: Payment <credential>. La respuesta lleva la clave API asignada en X-Machine-Session-Key (una sola vez) y el recibo en Payment-Receipt; use después esa clave como Bearer.
- Sin X-Payment-Method ni Accept: application/problem+json, una llamada no autenticada recibe 401 UNAUTHORIZED. Un depósito aún no confirmado da 402 PAYMENT_NOT_SETTLED; una credencial ilegible, 400 INVALID_CREDENTIAL. La clave asignada solo tiene los permisos send, status, balance y verify. Como máximo 5 desafíos por minuto y por IP.
curl -X GET "https://api.envoisms.ma/v1/machine-payments/status" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"enabled": true,
"deposit_usd": 5,
"credit_mad": 46.5,
"networks": ["base", "tempo"],
"permissions": ["send", "status", "balance", "verify"]
}Estadísticas de envío
Devuelve, sobre una ventana de días, los volúmenes, la entrega confirmada, el gasto cobrado y su desglose por canal, operador, estado y día.
| Parámetro | Tipo | Estado | Descripción |
|---|---|---|---|
| days | integer | Opcional | Tamaño de la ventana, 30 por defecto, 365 como máximo. |
| campaign_id | string | Opcional | Estadísticas de una sola campaña (la respuesta omite entonces error_diagnostics y waba_breakdown). |
- Permiso de clave API requerido: analytics.
- delivery_rate se calcula solo sobre los mensajes cuya entrega se confirmó o negó; coverage_pct indica qué parte de los mensajes recibió esa confirmación. Un valor desconocido es null, nunca 100. cost_* es lo cobrado, descontados los reembolsos. La respuesta se guarda en caché 60 segundos (cabecera X-Cache).
curl -X GET "https://api.envoisms.ma/v1/analytics" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"window_days": 30,
"campaign_id": null,
"summary": {
"total": 420,
"delivered": 398,
"failed": 6,
"unconfirmed": 16,
"delivery_rate": 98.51,
"coverage_pct": 96.19,
"cost_eur": 30.24,
"cost_mad": 332.64,
"avg_latency": 4.2
},
"channels": [
{ "channel": "sms", "total": 420, "cost_eur": 30.24, "cost_mad": 332.64, "fallback_sends": 0 }
],
"operators": [
{ "operator": "Maroc Telecom", "total": 250, "delivered": 240, "failed": 2, "unconfirmed": 8, "cost_eur": 18, "cost_mad": 198, "delivery_rate": 99.17, "coverage_pct": 96.8, "avg_latency": null }
],
"status_distribution": [
{ "status": "delivered", "total": 398 },
{ "status": "unconfirmed", "total": 16 },
{ "status": "failed", "total": 6 }
],
"daily": [
{ "day": "2026-10-21", "total": 14, "sms_total": 14, "whatsapp_total": 0, "delivered": 13, "failed": 0, "unconfirmed": 1, "cost_eur": 1.008, "cost_mad": 11.088 }
],
"error_diagnostics": [],
"waba_breakdown": []
}Listar claves API
Devuelve las claves API de la cuenta, activas y revocadas, nunca con su valor completo.
- Permiso de clave API requerido: api_keys.
- permissions e ip_whitelist son cadenas JSON (ip_whitelist vale null sin restricción).
curl -X GET "https://api.envoisms.ma/v1/api-keys" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "key_e84c2a6b8d0f1e3c5a7b9d2f",
"name": "Serveur de production",
"key_prefix": "smr_a8f7d6c5",
"ip_whitelist": "[\"196.200.1.4\"]",
"rate_limit": 180,
"permissions": "[\"send\",\"verify\",\"status\",\"balance\"]",
"active": 1,
"last_used_at": "2026-10-22 09:14:03",
"created_at": "2026-10-01 08:00:00",
"sandbox": false
}
]
}Crear una clave API
Crea una clave live (smr_…) o sandbox (env_test_…). Responde 201; el valor completo (api_key) se muestra una sola vez.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| name | string | Opcional | Etiqueta de la clave (por defecto: "API key"). |
| permissions | string[] | Opcional | Entre send, verify, status, balance, contacts, campaigns, webhooks, billing, analytics, api_keys, optouts, sender_id. Por defecto: send, verify, status, balance. Los valores desconocidos se ignoran. |
| ip_whitelist | string[] | Opcional | Únicas direcciones IP autorizadas a usar la clave. |
| rate_limit | integer | Opcional | Peticiones por minuto, de 10 a 2000. Por defecto según el plan: Starter 60, Business 180, Pro 500, Enterprise 1 200. |
| sandbox | boolean | Opcional | true crea una clave de prueba: las peticiones se validan y registran, no se envía ni se factura nada. El modo de una clave es definitivo. |
- Permiso de clave API requerido: api_keys.
- El número de claves live activas depende del plan, y las claves sandbox tienen su propio tope; por encima, 403 LIMIT_EXCEEDED. Una clave sandbox añade "note" a la respuesta.
curl -X POST "https://api.envoisms.ma/v1/api-keys" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "OTP backend",
"permissions": [
"verify",
"status"
],
"sandbox": true
}'{
"id": "key_e84c2a6b8d0f1e3c5a7b9d2f",
"name": "Serveur de production",
"key_prefix": "smr_a8f7d6c5",
"api_key": "smr_a8f7d6c5…",
"permissions": ["send", "verify", "status", "balance"],
"rate_limit": 180,
"sandbox": false,
"warning": "The full API key is shown once. Store it securely."
}Modificar una clave API
Modifica el nombre, los permisos, las IP autorizadas, el límite o el estado activo de una clave.
| Campo | Tipo | Estado | Descripción |
|---|---|---|---|
| name | permissions | ip_whitelist | mixed | Opcional | Mismas reglas que al crear. |
| rate_limit | integer | Opcional | Peticiones por minuto, de 10 a 1000. |
| active | boolean | Opcional | false suspende la clave. |
- Permiso de clave API requerido: api_keys.
- El modo sandbox o live no se cambia: 400 SANDBOX_IMMUTABLE. Cree una nueva clave.
curl -X PATCH "https://api.envoisms.ma/v1/api-keys/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "OTP backend (rotated)"
}'{
"updated": true,
"id": "key_e84c2a6b8d0f1e3c5a7b9d2f"
}Revocar una clave API
Revoca definitivamente una clave: deja de autenticar cualquier petición.
- Permiso de clave API requerido: api_keys.
curl -X DELETE "https://api.envoisms.ma/v1/api-keys/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"revoked": true,
"id": "key_e84c2a6b8d0f1e3c5a7b9d2f"
}