# Cómo migrar usuarios a un número nuevo de WhatsApp tras un baneo

> WhatsApp baneó el número de tu agente: los chats no se trasladan y el nuevo no puede escribir primero. Guía con código de webhook para migrar usuarios y su historial.

Source: https://whasp.ai/es/blog/migrar-usuarios-nuevo-numero-whatsapp-baneo · 2026-10-20

## Lo esencial

- Cuando WhatsApp banea un número de whasp, recibes un reemplazo sin costo con la misma etiqueta, el mismo perfil y el mismo periodo pagado, anunciado por un evento de webhook number.replaced, pero ningún chat se traslada.
- El número nuevo no puede escribir primero a nadie (409 chat_not_open), así que cada usuario tiene que escribirle, y la invitación debe viajar por un canal que sea tuyo, como el correo, push o un banner en la app.
- Envía a cada usuario un enlace construido con new_number.wa_link y un mensaje prefijado que lleve un código de un solo uso, e invita por tandas de unas 25 al día, porque un número admite 40 chats nuevos cada 24 horas.
- Indexa tu historial por tu propio id de usuario, no por el número, para que una persona que vuelve se reconozca por teléfono o por código y la conversación siga donde se quedó.
- Prepárate antes del primer baneo: un segundo canal recogido en el registro, un enlace de redirección estable, ese enlace en la línea about del perfil y un manejador de number.replaced probado.

Cuando WhatsApp banea un número, los chats no se trasladan: el reemplazo empieza vacío y no puede escribir primero a nadie. Para mover a tus usuarios, avísales por un canal que sea tuyo, envía a cada uno un enlace wa.me al número nuevo con un mensaje prefijado y reconócelos cuando escriban, para restaurar su historial desde tu propio almacén.

## ¿Qué pasa cuando WhatsApp banea el número de tu agente?

Cuando WhatsApp banea un número de whasp de forma definitiva, whasp lo retira y te pasa sin costo a un número listo. El reemplazo tiene la misma etiqueta, el mismo perfil (nombre, about, foto) y el mismo periodo pagado. Nada más se traslada. Ningún chat se mueve, y el número nuevo no tiene nada del historial anterior.

Tu webhook recibe un evento `number.replaced` con el número antiguo y el objeto del número nuevo, incluido su `wa_link`. Este es el evento, abreviado:

```json
{
  "id": "evt_9dK3mQ2vLx",
  "type": "number.replaced",
  "created_at": "2026-10-18T19:00:00Z",
  "data": {
    "old_number": "+15555550100",
    "new_number": {
      "number": "+15555550102",
      "wa_link": "https://wa.me/15555550102",
      "label": "ava",
      "status": "active",
      "profile": { "name": "Ava", "about": "Tu amiga IA", "synced": true },
      "paid_until": "2026-11-17T19:00:00Z"
    }
  }
}
```

También puede llegar un evento `number.status_changed`, cuando la salud del número antiguo pasa a `critical`. El orden de los eventos no está garantizado, así que no dependas de él. A partir de ese momento el número baneado no responde a nadie. Para entender por qué pasó, lee [por qué WhatsApp banea números](https://whasp.ai/es/blog/motivos-baneo-numero-whatsapp). Para la primera hora después de un baneo, incluida la [página de WhatsApp sobre bloqueos de cuenta](https://faq.whatsapp.com/465883178708358), mira [qué hacer cuando banean un número](https://whasp.ai/es/blog/numero-baneado-whatsapp-que-hacer).

## ¿Puede el número nuevo escribir primero a tus usuarios?

El número nuevo no puede escribir a nadie que no le haya escrito antes. Un envío a esa persona devuelve 409 `chat_not_open`. Vale para todos los números de whasp, también los de reemplazo, y es deliberado: los mensajes en frío son lo que hace que baneen números personales, y un número recién estrenado que abriera cientos de chats el primer día invitaría al siguiente baneo.

Así que el traslado tiene que empezar por el usuario. Debe tocar un enlace y escribir. Eso convierte la invitación en la parte difícil, porque no puede venir del chat de WhatsApp baneado ni del número nuevo. Tiene que viajar por un canal que sea tuyo. Cuando un usuario escribe, los envíos sin `from` salen del número nuevo, porque la API usa el número al que la persona escribió por última vez.

## ¿Cuál es el plan para mover a los usuarios?

El plan tiene siete pasos, y el código de abajo cubre los seis primeros.

1. **Verifica el evento** y deduplica por su id (los eventos pueden llegar dos veces, y la [especificación Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md) aconseja usar `webhook-id` como clave de idempotencia).
2. **Encuentra a los usuarios afectados**: aquellos cuyo chat vivía en `old_number`. Guarda el número en cada usuario a partir de `message.received`, donde `message.number` te dice qué número recibió el mensaje.
3. **Construye un enlace por usuario**: `new_number.wa_link` más `?text=` (el [formato "Haz clic para chatear"](https://faq.whatsapp.com/5913398998672934) de WhatsApp) con un mensaje corto que lleve un código de un solo uso.
4. **Envía el enlace por tu propio canal**: un banner en la app, una notificación push o un correo.
5. **Invita por tandas**, empezando por quienes estuvieron activos más recientemente.
6. **Reconoce a la persona cuando escriba**, restaura su historial y haz que el agente diga que el número cambió.
7. **Recuerda una vez y no insistas.** Pasada una semana, envía un solo recordatorio a quienes no han vuelto. El resto vuelve cuando quiera usar de nuevo el agente.

## ¿Cómo gestionas el evento y envías las invitaciones?

Verifica la firma sobre el cuerpo en bruto, deduplica por el id del evento y empieza las invitaciones. Este manejador reutiliza la comprobación de [Standard Webhooks](https://www.standardwebhooks.com) de la documentación de whasp.

```ts
import { randomBytes } from "node:crypto";
import { Webhook } from "standardwebhooks";

type User = { id: string; phone: string; number: string; lastActive: number; invited?: boolean };

declare const users: Map<string, User>; // tu base de datos
declare function notify(user: User, link: string): Promise<void>; // correo, push o banner en la app

const wh = new Webhook(process.env.WHASP_WEBHOOK_SECRET ?? ""); // whsec_...
const seen = new Set<string>(); // ids de eventos ya gestionados
export const codes = new Map<string, string>(); // código de invitación -> id de usuario
let moveFrom: { number: string; link: string } | null = null;
const DAILY_INVITES = 25; // por debajo del 80% de los 40 chats nuevos al día, con margen para registros

export async function POST(request: Request) {
  const body = await request.text(); // el cuerpo en bruto: verifica antes de parsear
  let event: any;
  try {
    event = wh.verify(body, Object.fromEntries(request.headers));
  } catch {
    return new Response("Invalid signature", { status: 401 });
  }
  if (seen.has(event.id)) return new Response(null, { status: 204 });
  seen.add(event.id);

  if (event.type === "number.replaced") {
    const { old_number, new_number } = event.data;
    moveFrom = { number: old_number, link: new_number.wa_link };
    await inviteBatch(); // en producción: responde 2xx primero y envía desde una cola
  }
  return new Response(null, { status: 204 });
}

// Ejecútalo una vez cuando llegue el evento y luego una vez al día hasta que no quede nadie.
export async function inviteBatch() {
  const target = moveFrom;
  if (!target) return;
  const batch = [...users.values()]
    .filter((u) => u.number === target.number && !u.invited)
    .sort((a, b) => b.lastActive - a.lastActive)
    .slice(0, DAILY_INVITES);
  for (const user of batch) {
    const code = randomBytes(4).toString("hex");
    codes.set(code, user.id);
    const text = encodeURIComponent(`Hola, soy yo otra vez (código ${code})`);
    await notify(user, `${target.link}?text=${text}`);
    user.invited = true;
  }
}
```

El código es aleatorio y de un solo uso, no el id del usuario, porque un mensaje prefijado es texto visible y editable y los ids no deben ir en los enlaces. Cambia los `Map` y el `Set` por tablas de base de datos.

El tamaño de la tanda sale de los límites. Un número de whasp admite 40 chats nuevos cada 24 horas y 100 chats activos en 30 días, y su salud pasa a `at_risk` cuando un límite supera el 80%, que son 32 chats nuevos. Unas 25 invitaciones al día dejan margen para registros nuevos. Un número con 100 usuarios activos tarda cuatro días, y las personas activas más recientes van primero porque son las que lo notarán.

### ¿Qué debe decir la invitación?

La invitación debe decir que el agente tiene un número nuevo, mostrar adónde lleva el enlace y sonar como un mensaje tuyo, no como una advertencia. Evita la palabra "baneado": a los usuarios les importa que Ava se haya mudado, no el motivo.

> **Asunto:** Ava tiene un número nuevo
>
> El antiguo número de WhatsApp de Ava dejó de funcionar. Toca abajo para seguir donde lo dejaste. El nombre y la foto son los mismos, y ella se acuerda de todo. [Abrir el chat]

Al tocar se abre WhatsApp con "Hola, soy yo otra vez (código 3f9a1c2e)" listo para enviar. Envía el enlace solo desde tu propia app o tu propio dominio. Un texto de un número desconocido que dice ser Ava parece phishing, y el número nuevo tampoco puede enviarlo. Pasada la primera semana, cuenta tres cosas: invitados, que volvieron y en silencio. A los que siguen en silencio les toca un recordatorio.

## ¿Cómo reconoces a un usuario que vuelve y restauras su historial?

Reconoce a la persona primero por teléfono y después por código, y carga la conversación con tu propio id de usuario. El número de teléfono de `chat` es el mismo en el número nuevo que en el antiguo, así que un usuario que vuelve se encuentra sin código. El código cubre a quien escribe desde un teléfono nuevo.

```ts
declare const users: Map<string, User>;
declare const codes: Map<string, string>;
declare function reply(userId: string, text: string, opts: { moved: boolean }): Promise<void>; // tu agente: carga el historial por id de usuario

export async function onInbound(m: { chat: string; number: string; text: string | null }) {
  const code = m.text?.match(/\(código ([0-9a-f]{8})\)/)?.[1];
  const byCode = code ? users.get(codes.get(code) ?? "") : undefined;
  const user = byCode ?? [...users.values()].find((u) => u.phone === m.chat);
  if (!user) return; // un desconocido: indícale el registro

  if (code) codes.delete(code);
  user.phone = m.chat;
  const moved = user.number !== m.number;
  user.number = m.number; // este chat ahora vive en el número nuevo
  // moved: haz que el agente diga "Soy yo. Mi número anterior dejó de funcionar y sigo acordándome de todo."
  await reply(user.id, m.text ?? "", { moved });
}
```

Llama a `onInbound` desde tu manejador de `message.received`. La regla de diseño detrás: indexa el historial por usuario, nunca por número. Si tu tabla de memoria usa el número de whasp como parte de la clave, un baneo deja huérfana cada conversación. [La memoria de conversación](https://whasp.ai/es/blog/memoria-agente-ia-whatsapp) muestra un esquema que sobrevive a un traslado.

Cuéntale a la persona lo que pasó. Un agente que saluda a un usuario que vuelve con su guion de bienvenida parece un desconocido. Basta una línea: es el mismo agente, el número anterior dejó de funcionar y la conversación continúa.

## ¿Qué pasa si un usuario no tiene otro canal?

Si WhatsApp es tu único canal con un usuario, no puedes llegar a él. El chat antiguo está en silencio y el número nuevo no puede escribir primero. Tres hábitos reducen esa brecha.

**Recoge un segundo canal en el registro.** Un correo o un inicio de sesión en la app cuesta un solo campo. Es el único camino de vuelta fiable.

**Publica un enlace estable.** Pon en tu propio dominio una redirección que pida a `GET /v1/numbers/pick` un número sano en el momento del clic. Usa ese enlace donde tiene que durar, como tu app, las bios y los códigos QR, en lugar de un enlace wa.me directo.

```ts
// GET https://chat.example.com/ava: el único enlace que imprimes, publicas y pones en los correos.
export async function GET() {
  const res = await fetch(
    `https://api.whasp.ai/v1/numbers/pick?text=${encodeURIComponent("Hola, soy yo")}`,
    { headers: { Authorization: `Bearer ${process.env.WHASP_API_KEY}` } },
  );
  if (!res.ok) return new Response("Inténtalo de nuevo en un minuto.", { status: 503 }); // 409 no_capacity
  const { wa_link } = await res.json();
  return Response.redirect(wa_link, 302);
}
```

**Pon ese enlace en el perfil.** Un reemplazo conserva el perfil, y la línea about admite hasta 139 caracteres en ASCII simple por ahora. Una URL corta ahí viaja a cada reemplazo futuro y está donde los usuarios pueden leerla. [Los enlaces wa.me y los códigos QR](https://whasp.ai/es/blog/enlace-wa-me-mensaje-prellenado-qr) explican dónde colocarlos.

## ¿Qué debes preparar antes del primer baneo?

Cualquier número manejado como lo haría una persona puede ser baneado, así que prepárate mientras todo está tranquilo.

- **Prueba el manejador** con un payload de ejemplo de `number.replaced`, incluidos un duplicado y una firma incorrecta.
- **Prevé un evento perdido.** whasp reintenta una entrega hasta 12 veces con backoff, y luego se rinde y descarta el cuerpo, así que no puede reenviarse. `GET /v1/webhook/deliveries` lista los últimos 7 días. `GET /v1/numbers` lista tus números: uno que no reconozcas es el reemplazo, y los campos `status` y `replaced_by` muestran qué pasó con el antiguo.
- **Usa más de un número** cuando puedas. Así, un baneo solo mueve a los usuarios de ese número. [Cuántos números necesita tu agente](https://whasp.ai/es/blog/cuantos-numeros-whatsapp-agente-ia) tiene las cuentas.
- **Indexa el historial por usuario** y guarda un número de teléfono en cada registro de usuario.

## ¿Qué debes hacer ahora?

Añade esta semana el manejador de `number.replaced`, y después pon el enlace estable en tu app y en la línea about del perfil. A partir de ahora recoge un correo en el registro. La API completa está en la [documentación](https://whasp.ai/docs).

## Preguntas

### ¿Qué pasa con mis chats cuando WhatsApp banea un número?

En whasp, nada se traslada. El número baneado se retira y recibes un reemplazo con la misma etiqueta, el mismo perfil y el mismo periodo pagado. El número nuevo empieza sin chats, y tu sistema guarda el historial, así que lo restauras cuando cada usuario te escribe.

### ¿Puede el número de reemplazo escribir primero a mis usuarios actuales?

No. Un envío a alguien que nunca escribió a ese número devuelve 409 chat_not_open, y la regla vale también para los números de reemplazo. Los usuarios deben escribir primero, normalmente tocando un enlace wa.me que les envías por correo, push o dentro de tu app.

### ¿A cuántos usuarios puedo mover al número nuevo por día?

Un número de whasp admite 40 chats nuevos cada 24 horas y 100 chats activos en 30 días, y pasa a at_risk por encima del 80% de un límite. Unas 25 invitaciones al día te mantienen por debajo y dejan margen para registros nuevos.

### ¿Cómo aviso a los usuarios del número nuevo si solo tengo su chat de WhatsApp?

No puedes llegar a ellos desde el número nuevo, y el chat baneado está en silencio. Prepárate antes: recoge un correo o un inicio de sesión en la app en el registro, y publica un enlace estable en tu propio dominio que siempre redirija a un número sano.

### ¿Y si mi webhook perdió el evento number.replaced?

whasp reintenta una entrega hasta 12 veces con backoff, y luego se rinde y descarta el cuerpo. Lista tus números con GET /v1/numbers: uno que no reconozcas es el reemplazo, y los campos status y replaced_by muestran qué pasó con el antiguo.
