Volver al blog
Desarrolladores

Cómo saber si podés mandar texto libre: serviceWindowExpiresAt en la API de Waspy

serviceWindowExpiresAt viene en GET /conversations/:id y en el listado. resolve y by-phone no lo traen. Si está vacío o vencido, un texto responde 403 SERVICE_WINDOW_EXPIRED.

· 12 min
Cómo saber si podés mandar texto libre: serviceWindowExpiresAt en la API de Waspy

Respuesta corta: antes de un texto libre, leé serviceWindowExpiresAt. Está en GET https://api.waspytech.com/api/v2/conversations/:id y en el listado GET /api/v2/conversations. Si la fecha es null o ya pasó, POST /messages con type: "text" responde 403 SERVICE_WINDOW_EXPIRED. Ahí el pedido que sale es una plantilla.

Lo confirmé contra el código el 1 de noviembre de 2026. GET /conversations/resolve y GET /conversations/by-phone/:phone siguen sin traer ese campo. La nota del 31 de octubre decía lo mismo: no cambió. Cómo se busca el teléfono está en resolver la conversación.

Dónde está, y dónde no

Pedido¿Trae serviceWindowExpiresAt?
GET /conversations/:idSí. ISO-8601 o null
GET /conversationsSí, en cada ítem
GET /contacts/:id/conversationsSí. Scope contacts:read
GET /conversations/by-phone/:phoneNo. conversationId, contactName, contactPhone
GET /conversations/resolveNo. conversationId, match, alternatives, phoneHasWhatsApp
GET /contacts/:id/can-send-textNo existe
Limitaciones todavía recomienda leer la fecha en by-phone. El schema ConversationByPhoneResponse no tiene esa propiedad. Si tu cliente fue generado contra esa frase, va a leer undefined y va a mandar el texto igual.

No hay un booleano canSendText. La regla la aplicás vos con la fecha, y el servidor la vuelve a aplicar al enviar.

El GET

curl -sS "https://api.waspytech.com/api/v2/conversations/UUID-DEL-CHAT" \
  -H "Authorization: Bearer $WASPY_API_KEY"
{
  "data": {
    "id": "UUID-DEL-CHAT",
    "contactId": "UUID-DEL-CONTACTO",
    "phoneNumberId": "UUID-DEL-CANAL",
    "status": "open",
    "serviceWindowExpiresAt": "2026-11-02T18:40:00.000Z",
    "contact": { "name": "María Pérez", "phoneNumber": "+5491126032641", "email": null }
  },
  "meta": { "requestId": "..." }
}
ValorQué podés mandar
Un ISO en el futuroTexto, foto, audio, documento, interactivo, reacción. También una plantilla
nullSolo plantilla. El envío trata el vacío como ventana cerrada
Un ISO que ya pasóSolo plantilla
La ventana de negocio dura 24 horas desde el mensaje de la persona, y se renueva si vuelve a escribir. El campo es el vencimiento guardado, no un contador de “horas que te quedan”. La regla, del lado del inbox, está en la ventana de 24 horas y en la ficha ventana de 24 horas.

El 403

{
  "error": {
    "code": "SERVICE_WINDOW_EXPIRED",
    "message": "Service window expired. Only templates can be sent outside the 24h window",
    "status": 403
  },
  "meta": { "requestId": "..." }
}

Ese código lo lanza el envío cuando el tipo no es template y la fecha falta o venció. Un type: "template" no entra en ese if. Meta igual puede rechazar la plantilla por otra causa (nombre, idioma, parámetros): eso llega después, en message.status_changed, no como este 403.

Texto o plantilla

SituaciónPedidoQué cobra Meta, Argentina, desde el 1 de octubre de 2026
La fecha es futura y el cliente preguntó “¿salió?”type: "text"Servicio: 1.000 por número y por mes; después ~ARS 37,68 (~US$0,026)
La fecha es null o venció, y hay que avisar el despachotype: "template", utilidad, sin cupón~ARS 37,68 (~US$0,026) por mensaje entregado
El chat está abierto, pero reenviás la plantillatype: "template"Se cobra igual. Desde el 1 de octubre la utilidad dentro de la ventana no es gratis
El texto lleva un “15% en la próxima” y sale como plantillaMarketing~ARS 89,56 (~US$0,0618)
El cupo de 1.000 es de servicio (texto, foto, audio, documento), no de plantillas. No se acumula al mes siguiente. La rate card y el sitio de la plataforma son las dos páginas de Meta: la de desarrolladores es la que usamos para el precio. El desglose está en cuánto cuesta la API.

Si la persona escribió desde un anuncio de clic a WhatsApp, hay un segundo reloj (hasta 7 días en la documentación de desarrolladores; la página comercial, leída el 1 de noviembre de 2026, sigue en 72 horas). Ese reloj no alarga esta fecha. Pasadas las 24 horas el texto sigue en 403, aunque el punto de entrada siga abierto: lo que sale es la plantilla. El cruce está en la ventana de 7 días.

Node

const base = 'https://api.waspytech.com/api/v2';
const headers = { Authorization: 'Bearer ' + process.env.WASPY_API_KEY };

async function canSendText(conversationId) {
  const res = await fetch(base + '/conversations/' + conversationId, { headers });
  if (!res.ok) throw new Error('GET conversation ' + res.status);
  const body = await res.json();
  const exp = body.data && body.data.serviceWindowExpiresAt;
  return Boolean(exp) && new Date(exp).getTime() > Date.now();
}

async function reply(conversationId, phoneNumberId, to, text, template) {
  const open = await canSendText(conversationId);
  const payload = open
    ? { phoneNumberId, to, type: 'text', text: { body: text } }
    : { phoneNumberId, to, type: 'template', template };
  const res = await fetch(base + '/messages', {
    method: 'POST',
    headers: { ...headers, 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });
  return res.json();
}

Python

import os
from datetime import datetime, timezone
import requests

BASE = "https://api.waspytech.com/api/v2"
HEADERS = {"Authorization": "Bearer " + os.environ["WASPY_API_KEY"]}

def can_send_text(conversation_id: str) -> bool:
    res = requests.get(f"{BASE}/conversations/{conversation_id}", headers=HEADERS, timeout=20)
    res.raise_for_status()
    exp = res.json()["data"].get("serviceWindowExpiresAt")
    if not exp:
        return False
    return datetime.fromisoformat(exp.replace("Z", "+00:00")) > datetime.now(timezone.utc)

El GET previo y el POST no son atómicos: la ventana puede vencer en el medio. Si el POST vuelve 403, reintentá con la plantilla. No reintentes el texto.

La API pública está en Scale y Enterprise. El inbox, donde una persona ve el contador sin este GET, está desde el plan de entrada. Los dos están en Planes y en la API. Diez días sin tarjeta, en el registro.

Tu equipo atendiendo por WhatsApp en minutos

CRM facil pero potente. En 10 minutos tenes a tu equipo trabajando — no necesitas hablar con nadie.