13 de septiembre de 2026

WhatsApp API, 1 de octubre: qué cambia en tu código cuando Meta cobra los mensajes de servicio

Foto de Marco Orta Marco Orta | 19 min de lectura
Compartir
Portada tipográfica: WhatsApp Cloud API y un diff de dos líneas donde pricing.type pasa de free_customer_service a regular
Tabla de Contenidos

    El 1 de octubre de 2026 el webhook de estados de WhatsApp no cambia de forma: cambia de significado. El mismo objeto pricing que hoy llega con "type": "free_customer_service" empieza a llegar con "type": "regular" en dos casos que hasta el 30 de septiembre son gratis: los mensajes de servicio a partir del 1,001 del mes en cada número, y las plantillas de utilidad que mandas dentro de la ventana de 24 horas. Si tu código tiene la tarifa de servicio fija en cero, decide con billable o descarta los estados cuya categoría no reconoce, tu reporte va a decir que no gastaste nada mientras Meta te cobra.

    De precios en español ya se escribió mucho. Este post va de lo otro: qué campos mirar, qué valores trae cada uno según la documentación de Meta, cómo contar el cupo gratis sin inventarte la regla y qué revisar antes del 30 de septiembre. Todas las cifras son de la tarjeta de México en pesos.

    Qué se cobra antes y después del 1 de octubre

    Tarifas por mensaje entregado a un número con lada +52, en MXN. Salen de las tarjetas oficiales «effective July 1, 2026» y «effective October 1, 2026», que descargué el 13 de septiembre desde la página de precios de Meta (fila Mexico).

    MensajeHasta el 30-sepDesde el 1-octpricing.type desde el 1-oct
    Plantilla de marketing0.56140.7298regular
    Plantilla de utilidad fuera de la ventana0.15650.1565regular
    Plantilla de utilidad dentro de la ventana de 24 hgratis0.1565, desde la primeraregular
    Plantilla de autenticación0.15650.1565regular
    Servicio (sin plantilla), del 1 al 1,000 del mes por númerogratisgratisfree_customer_service
    Servicio (sin plantilla), del 1,001 en adelantegratis0.1565regular
    Cualquiera dentro de la ventana de punto de entrada gratuito (72 h)gratisgratisfree_entry_point

    Tres detalles que cambian cómo lo programas:

    • El cambio entra a las 00:00 en la zona horaria de tu WABA, no en UTC. La página de precios lo dice para todo el paquete de octubre: «Rate updates below apply as of 12am by WhatsApp Business Account (WABA) timezone».
    • El cupo de 1,000 es solo de servicio. Meta lo describe como «a free monthly tier of 1,000 service messages per business phone number», que no se acumula de un mes a otro. No anuncia ningún cupo para utility: una plantilla de utilidad dentro de la ventana se cobra desde la primera.
    • El marketing sube casi 30 % en México (0.5614 → 0.7298). Es el único cambio de tarifa de la fila; utility, autenticación y servicio quedan en 0.1565. Y los niveles por volumen no te van a salvar: en la tarjeta de niveles de octubre, la utility de México se cobra a precio de lista del mensaje 0 al 1,000,000 del mes, y el servicio no tiene niveles en absoluto según la página de mensajes sin plantilla.

    El payload real y los campos que importan

    Así llega un mensaje de servicio entregado después de agotar el cupo del mes. La estructura es la de la referencia del webhook de estados y el objeto pricing es literal el que publica Meta para «Paid service message». Los IDs y teléfonos son los de ejemplo de la propia referencia; el timestamp es el 14 de octubre de 2026.

    {
      "object": "whatsapp_business_account",
      "entry": [
        {
          "id": "102290129340398",
          "changes": [
            {
              "value": {
                "messaging_product": "whatsapp",
                "metadata": {
                  "display_phone_number": "15550783881",
                  "phone_number_id": "106540352242922"
                },
                "statuses": [
                  {
                    "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQUFERjg0NDEzNDdFODU3MUMxMAA=",
                    "status": "delivered",
                    "timestamp": "1792002600",
                    "recipient_id": "16505551234",
                    "pricing": {
                      "billable": true,
                      "pricing_model": "PMP",
                      "type": "regular",
                      "category": "service"
                    }
                  }
                ]
              },
              "field": "messages"
            }
          ]
        }
      ]
    }
    

    El mismo mensaje, en septiembre o dentro del cupo, trae "billable": false y "type": "free_customer_service". Una utility dentro de la ventana pasa exactamente igual: free_customer_service hasta el 30 de septiembre, regular desde el 1 de octubre, con "category": "utility".

    pricing.type: el que decide

    Es el campo que dice por qué un mensaje se cobra o no. La referencia de estados documenta tres valores:

    • regular: se cobra.
    • free_customer_service: gratis por ir dentro de la ventana de atención. Desde octubre, eso incluye estar dentro del cupo de 1,000.
    • free_entry_point: gratis por la ventana de 72 horas que abren los anuncios Click-to-WhatsApp y los botones de llamada a la acción de una página de Facebook.

    La página de precios añade un cuarto que la referencia (actualizada el 21 de mayo) todavía no lista: free_group_customer_service, para mensajes a grupos.

    pricing.category: qué tarifa se aplicó

    Valores de la referencia: authentication, authentication-international, marketing, marketing_lite, referral_conversion, service y utility. Ojo con authentication-international: en pricing.category lleva guion, mientras que la categoría de conversation.origin.type lleva guion bajo. La página de precios añade group_service y group_utility para grupos.

    La consecuencia práctica: no valides estos campos contra una lista cerrada. Si tu parser descarta un estado porque la categoría no está en tu enum, ese mensaje se queda sin precio en tu base mientras Meta sí lo cobra.

    pricing.billable: no decidas con él

    La referencia es explícita: «The billable property will be deprecated in a future versioned release. Use pricing.type and pricing.category together to determine whether a message is billable and, if so, its billing rate.» Hoy billable y type coinciden. El día que Meta lo quite en una versión nueva de la Graph API, un if (billable === false) return 0 se convierte en «todo cuesta» o en «nada cuesta», según cómo trates el undefined.

    Cuándo llega el objeto, y cuándo cuenta

    • Según la sintaxis de la referencia, pricing viene «only included with sent status, and one of either delivered or read status». El ejemplo de la v24 aclara que puede venir solo en el delivered y no en el sent. Así que te va a llegar dos veces o una, y tienes que deduplicar por wamid.
    • Meta cobra lo entregado, no lo enviado («only when the message is delivered (vs. sent)»). Y el delivered puede no llegar nunca: si el usuario tiene el chat abierto, Meta manda directo el read. Cuenta un mensaje como entregado cuando llega delivered o read.
    • Desde la v24.0 el objeto conversation ya no viene, salvo dentro de una ventana de punto de entrada gratuito. Si tu código sacaba la categoría de conversation.origin.type, ya iba tarde.

    Los cuatro errores que hacen que tu reporte diga cero

    Revisando para este post el agente de WhatsApp que tengo en desarrollo, encontré dos de estos en mi propio código. Los cuatro son fáciles de tener:

    1. Tarifa sin fecha de vigencia. Una constante service: 0 calcula bien hasta el 30 de septiembre y mal desde el 1 de octubre, aunque el webhook ya diga regular. Meta solo puede cambiar tarifas el día 1 de cada trimestre (enero, abril, julio y octubre), con un mes de aviso para una actualización de tarjeta: modela la tarjeta con su fecha de entrada en vigor.
    2. Decidir con billable. Funciona hoy; deja de funcionar cuando Meta lo retire.
    3. Lista blanca de categorías. group_service, free_group_customer_service o el guion de authentication-international acaban como «sin categoría» y valen cero en tu suma.
    4. Contar cada estado. Si sumas en cada webhook con pricing, un mismo mensaje cuenta doble (sent + delivered), y un mensaje que falló después de enviarse cuenta aunque Meta no lo cobre.

    Código: contar lo facturable por número y por mes

    La idea es la del libro de costos de mi agente: una fila por mensaje saliente, que se va rellenando con lo que traiga cada estado, y el costo se calcula al leer. Está escrito para Cloudflare D1 (SQLite), pero el SQL es portable.

    Primero la tabla. category y pricing_type se guardan tal cual llegan, sin lista blanca:

    CREATE TABLE wa_outbound (
      wamid           TEXT PRIMARY KEY,
      phone_number_id TEXT NOT NULL,
      month           TEXT NOT NULL,              -- 'YYYY-MM' en la zona horaria de la WABA
      delivered       INTEGER NOT NULL DEFAULT 0, -- 1 cuando llegó `delivered` o `read`
      category        TEXT,                       -- pricing.category, sin normalizar
      pricing_type    TEXT                        -- pricing.type, sin normalizar
    );
    CREATE INDEX wa_outbound_month ON wa_outbound (phone_number_id, month);
    

    Después, lo que haces con cada statuses[]. El phone_number_id sale de value.metadata.phone_number_id, que es la unidad del cupo:

    type Open<T extends string> = T | (string & {}); // autocompleta, pero acepta lo que Meta añada
    
    export interface StatusPricing {
      /** Meta lo va a deprecar: decide con `type` + `category`. */
      billable?: boolean;
      pricing_model: Open<'PMP' | 'CBP'>;
      type: Open<'regular' | 'free_customer_service' | 'free_entry_point' | 'free_group_customer_service'>;
      category: Open<'service' | 'utility' | 'marketing' | 'marketing_lite' | 'authentication' | 'authentication-international' | 'referral_conversion' | 'group_service' | 'group_utility'>;
    }
    
    export interface WebhookStatus {
      id: string; // wamid
      status: Open<'sent' | 'delivered' | 'read' | 'failed' | 'played'>;
      timestamp: string; // epoch en segundos, como cadena
      recipient_id: string;
      pricing?: StatusPricing;
    }
    
    /** 'YYYY-MM' en la zona horaria de tu WABA, p. ej. 'America/Mexico_City'. */
    export function monthKey(epochSeconds: number, timeZone: string): string {
      return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit' })
        .format(new Date(epochSeconds * 1000))
        .slice(0, 7);
    }
    
    const DELIVERED = new Set(['delivered', 'read']); // si llega `read` sin `delivered`, se entregó
    
    export async function applyStatus(db: D1Database, phoneNumberId: string, s: WebhookStatus, timeZone: string) {
      await db
        .prepare(
          `INSERT INTO wa_outbound (wamid, phone_number_id, month, delivered, category, pricing_type)
           VALUES (?1, ?2, ?3, ?4, ?5, ?6)
           ON CONFLICT(wamid) DO UPDATE SET
             month        = CASE WHEN excluded.delivered = 1 AND delivered = 0 THEN excluded.month ELSE month END,
             delivered    = MAX(delivered, excluded.delivered),
             category     = COALESCE(excluded.category, category),
             pricing_type = COALESCE(excluded.pricing_type, pricing_type)`,
        )
        .bind(
          s.id,
          phoneNumberId,
          monthKey(Number(s.timestamp), timeZone),
          DELIVERED.has(s.status) ? 1 : 0,
          s.pricing?.category ?? null,
          s.pricing?.type ?? null,
        )
        .run();
    }
    

    El ON CONFLICT hace el trabajo sucio: los estados repetidos no duplican, el pricing se conserva venga en el sent o en el delivered, y el mes que cuenta es el de la entrega (un mensaje enviado el 30 de septiembre a las 23:59 y entregado el 1 de octubre cae en octubre).

    Por último, la cuenta del mes. La tarjeta lleva fecha, y lo que decide si se cobra es type, no tu contador:

    const FREE_SERVICE_PER_MONTH = 1000;
    
    /** Tarjeta de México en MXN por mensaje entregado. Una entrada por trimestre con cambios. */
    const MX_RATES_MXN: Array<{ from: string; rates: Record<string, number> }> = [
      { from: '2026-07', rates: { marketing: 0.5614, utility: 0.1565, authentication: 0.1565 } },
      { from: '2026-10', rates: { marketing: 0.7298, utility: 0.1565, authentication: 0.1565, service: 0.1565 } },
    ];
    
    const ratesFor = (month: string) => MX_RATES_MXN.findLast((card) => card.from <= month)?.rates ?? {};
    
    export async function monthlyBill(db: D1Database, phoneNumberId: string, month: string) {
      const { results } = await db
        .prepare(
          `SELECT category, pricing_type, COUNT(*) AS n FROM wa_outbound
           WHERE phone_number_id = ? AND month = ? AND delivered = 1
           GROUP BY category, pricing_type`,
        )
        .bind(phoneNumberId, month)
        .all<{ category: string | null; pricing_type: string | null; n: number }>();
    
      const rates = ratesFor(month);
      let mxn = 0;
      let serviceDelivered = 0;
      let paidService = 0;
      const unpriced: Record<string, number> = {};
    
      for (const { category, pricing_type, n } of results) {
        if (!category || !pricing_type) {
          unpriced['(sin pricing)'] = (unpriced['(sin pricing)'] ?? 0) + n;
          continue;
        }
        // Meta no documenta si los de punto de entrada gastan cupo: aquí no se cuentan.
        if (category === 'service' && pricing_type !== 'free_entry_point') serviceDelivered += n;
        if (pricing_type !== 'regular') continue; // gratis: ventana, cupo o punto de entrada
        if (category === 'service') paidService += n;
    
        const rate = rates[category];
        if (rate === undefined) {
          unpriced[category] = (unpriced[category] ?? 0) + n; // se cobra y no tienes tarifa: no lo sumes como 0
          continue;
        }
        mxn += n * rate;
      }
    
      return {
        mxn: Math.round(mxn * 100) / 100,
        serviceDelivered,
        freeServiceLeft: Math.max(0, FREE_SERVICE_PER_MONTH - serviceDelivered),
        paidService,
        unpriced,
      };
    }
    

    Con eso, la alerta es una línea en un cron diario: avisa cuando freeServiceLeft baje de 200, cuando aparezca el primer paidService del mes o cuando unpriced no venga vacío. Y si paidService es mayor que cero con serviceDelivered por debajo de 1,000, tu contador y Meta no coinciden: puede ser otra aplicación mandando desde el mismo número, webhooks perdidos o el corte de mes en otra zona horaria. Manda Meta.

    Para cruzarlo a fin de mes, el campo pricing_analytics de la WABA acepta las dimensiones PHONE, PRICING_CATEGORY y PRICING_TYPE. Dos avisos de la misma página: los datos son aproximados («may differ from what’s shown on invoices»), y COST no viene si tu WABA usa la línea de crédito de un proveedor. Además, la descripción de SERVICE en esa referencia (actualizada el 11 de junio) todavía dice «Messages that were not charged»; la página de mensajes sin plantilla ya muestra la consulta con REGULAR + SERVICE.

    Cuánto cuesta en pesos: una PyME con 600 conversaciones al mes

    Un consultorio o un taller con un agente que atiende WhatsApp, un solo número, clientes con lada +52 y sin anuncios Click-to-WhatsApp. Al mes:

    • 600 conversaciones, con 5 mensajes de servicio del agente cada una: 3,000 mensajes de servicio.
    • 600 confirmaciones con plantilla de utilidad, mandadas dentro de la ventana.
    • 400 recordatorios con plantilla de utilidad, fuera de la ventana.
    • Una campaña de marketing a 500 contactos.

    Supongo que todo se entrega; en la vida real sale un poco menos, porque Meta solo cobra lo entregado. Tarifas de lista, sin impuestos.

    Septiembre (tarjeta de julio):

    ConceptoCuentaMXN
    Servicio3,000 × 00.00
    Utility dentro de la ventana600 × 00.00
    Utility fuera de la ventana400 × 0.156562.60
    Marketing500 × 0.5614280.70
    Total343.30

    Octubre (tarjeta de octubre):

    ConceptoCuentaMXN
    Servicio(3,000 − 1,000) × 0.1565 = 2,000 × 0.1565313.00
    Utility dentro de la ventana600 × 0.156593.90
    Utility fuera de la ventana400 × 0.156562.60
    Marketing500 × 0.7298364.90
    Total834.40

    La factura de Meta pasa de 343.30 a 834.40 pesos: 491.10 MXN más al mes. No es una cifra que quiebre a nadie, pero es 2.4 veces la de septiembre, y 406.90 de esos 491.10 pesos (más del 80 %) salen de dos líneas que hoy valen cero.

    Dónde está la palanca de verdad: en tu código

    Pasar las confirmaciones de plantilla de utilidad a texto libre dentro de la ventana no ahorra nada en este ejemplo: la tarifa de servicio y la de utilidad son la misma en México (0.1565) y el cupo de 1,000 ya se agotó. Solo ayuda a negocios que se quedan por debajo de 1,000 mensajes de servicio al mes.

    Lo que sí mueve la cifra es cuántos mensajes manda tu agente por respuesta. Muchos agentes de IA parten cada respuesta en tres o cuatro burbujas porque se lee más natural. Desde el 1 de octubre, cada burbuja pasado el mensaje 1,000 es un cargo. Si el mismo agente contesta con 3 mensajes por conversación en vez de 5:

    • 600 × 3 = 1,800 mensajes de servicio.
    • (1,800 − 1,000) × 0.1565 = 800 × 0.1565 = 125.20 MXN, contra 313.00.
    • Ahorro: 187.80 MXN al mes, sin tocar un solo precio.

    Si parte de tus clientes llega por anuncios Click-to-WhatsApp, responder dentro de las primeras 24 horas abre la ventana de punto de entrada de 72 horas, y ahí todo sale como free_entry_point. Meta confirma que esa ventana no cambia para la entrega de mensajes.

    Qué pasa si no tienes método de pago

    Aquí Meta tiene dos redacciones, de fechas distintas, y conviene leerlas juntas.

    La página de mensajes sin plantilla, actualizada el 25 de agosto de 2026, dice:

    For any Solution Provider or directly-integrated businesses that does not have a payment method on file by September 30, 2026, Meta will stop delivering service messages as of when they become charged on October 1, 2026. To avoid disruptions to your service messages, please add a payment method for your WhatsApp Business Account(s) by September 30, 2026.

    La página de precios, actualizada el 10 de septiembre de 2026, que es la que introduce el cupo gratis como novedad, precisa:

    If you do not have a payment method for your WhatsApp Business account: Meta will deliver your first 1,000 service messages each month but not deliver as of your 1,001st.

    No se contradicen: la primera dice que se dejan de entregar «cuando empiezan a cobrarse», y la segunda, escrita después del cupo, dice que eso ocurre en el mensaje 1,001. En la práctica:

    • La fecha que da Meta para tener el método de pago es el 30 de septiembre. No esperes a ver el primer fallo.
    • Sin método de pago, tu agente funciona normal hasta que el número cruza los 1,000 mensajes de servicio del mes, y ahí deja de entregar a mitad de mes. En un negocio con volumen, eso puede ser un martes a media tarde.
    • Meta no publica, en las páginas que revisé, qué código de error trae el estado failed en ese caso. Vigila un aumento de failed en mensajes de servicio desde el 1 de octubre.
    • El aviso de agosto va dirigido a «Solution Provider or directly-integrated businesses». Si tu WABA factura a través de un proveedor de soluciones, pídele por escrito que confirme que el método de pago está resuelto antes del 30 de septiembre.

    Checklist antes del 30 de septiembre

    1. Método de pago en el Billing Hub de cada WABA, o confirmación escrita de tu proveedor si facturas a través de él.
    2. Parser tolerante: pricing.type y pricing.category se guardan tal cual. Nada de descartar el estado porque trae group_service, free_group_customer_service o authentication-international.
    3. Deja de decidir con billable. Usa type === 'regular' más la tarifa de category.
    4. Tarjeta con fecha de vigencia, cargada con la de octubre (marketing a 0.7298 en México). La próxima fecha en que Meta puede cambiar tarifas es el 1 de enero de 2027.
    5. Cuenta en delivered o read, una vez por wamid, y agrupa por phone_number_id y mes en la zona horaria de tu WABA.
    6. Alertas: quedan menos de 200 de servicio gratis, primer servicio regular del mes, categorías sin tarifa y aumento de failed.
    7. Plantillas de utilidad dentro de la ventana: desde el 1 de octubre se cobran desde la primera. Si mandas menos de 1,000 mensajes de servicio al mes, un texto libre dentro de la ventana puede salir gratis donde la plantilla cuesta.
    8. Plantillas de marketing contra utilidad: Meta te cobra la categoría que tenga la plantilla al momento de usarla, y puede recategorizarla. Suscríbete al webhook template_category_update, que avisa 24 horas antes de un cambio automático de categoría.
    9. Mensajes por respuesta: junta las burbujas de tu agente. Es la palanca más barata del ejemplo.
    10. Cruce mensual contra pricing_analytics, sabiendo que es aproximado.

    Si prefieres que alguien revise tu integración, tus plantillas y cuánto te va a costar octubre antes de que llegue, es justo lo que hago en WhatsApp para tu negocio.

    Para seguir leyendo:

    Preguntas frecuentes

    ¿Qué cambia en la API de WhatsApp el 1 de octubre de 2026?

    Meta empieza a cobrar dos tipos de mensaje que hoy son gratis: los mensajes de servicio (los que mandas sin plantilla dentro de la ventana de 24 horas) a partir del mensaje 1,001 de cada mes por número de negocio, y las plantillas de utilidad que mandas dentro de esa ventana, que se cobran desde la primera. En México, ambos a 0.1565 MXN por mensaje entregado. Además, la tarifa de marketing de México sube de 0.5614 a 0.7298 MXN. El cambio entra a las 00:00 en la zona horaria de tu cuenta de WhatsApp Business.

    ¿Cómo sé en el webhook si un mensaje de WhatsApp se cobró?

    Mira pricing.type en el webhook de estados: regular significa que se cobra; free_customer_service, free_entry_point o free_group_customer_service significan que salió gratis. pricing.category te dice qué tarifa se aplicó. No uses pricing.billable para decidir: la referencia de Meta dice que se va a deprecar en una versión futura y recomienda usar type y category juntos.

    ¿Los 1,000 mensajes gratis de WhatsApp son por cuenta o por número?

    Por número de teléfono de negocio. Meta dice que cada número recibe 1,000 mensajes de servicio gratis al mes, que cobra desde el mensaje de servicio 1,001 entregado ese mes y que el cupo no se acumula: se reinicia cada mes. El cupo es solo de mensajes de servicio; las plantillas de utilidad dentro de la ventana no tienen cupo gratis.

    ¿Qué pasa si no tengo método de pago en WhatsApp Business el 1 de octubre?

    Según la página de precios de Meta actualizada el 10 de septiembre de 2026, Meta entrega tus primeros 1,000 mensajes de servicio del mes y deja de entregarlos a partir del 1,001. La página de mensajes sin plantilla pide tener el método de pago en el Billing Hub antes del 30 de septiembre de 2026. Si facturas a través de un proveedor de soluciones, confirma con él que el método de pago está resuelto.

    ¿El objeto pricing llega en todos los estados del webhook?

    No. Según la referencia de Meta, pricing viene con el estado sent y con uno de delivered o read, y en la v24 puede venir solo en delivered. Como Meta cobra lo entregado y a veces manda read sin delivered, cuenta cada mensaje una sola vez por wamid cuando llegue delivered o read, y guarda el pricing venga en el estado que venga.

    ¿Cuánto va a pagar a Meta una pyme mexicana desde octubre?

    Depende del volumen. Con 600 conversaciones al mes de 5 mensajes de servicio cada una, 600 confirmaciones de utilidad dentro de la ventana, 400 recordatorios de utilidad fuera de ella y una campaña de marketing a 500 contactos, la factura de Meta pasa de 343.30 MXN en septiembre a 834.40 MXN en octubre con tarifas de lista. Reducir el agente de 5 a 3 mensajes por conversación baja la parte de servicio de 313.00 a 125.20 MXN.

    ¿Conviene cambiar plantillas de utilidad por texto libre dentro de la ventana?

    Solo si mandas menos de 1,000 mensajes de servicio al mes. En México el servicio y la utilidad cuestan lo mismo (0.1565 MXN) desde el 1 de octubre, pero el servicio tiene 1,000 gratis al mes por número y la utilidad no. Una vez agotado el cupo, da igual. La palanca más grande suele ser que tu agente mande menos mensajes por respuesta.

    Compartir

    Buscar

    Etiquetas

    Migración IA PHP JavaScript Laravel Tutorial Desarrollo Web Upgrade Buenas Prácticas Seguridad SEO Backend Claude Laravel 13 TypeScript