WhatsApp Web en Rust, para bots en cualquier lenguaje.
SoblendWA habla el protocolo de WhatsApp Web directamente: WebSocket, Noise, nodos binarios y cifrado Signal, sin navegador ni Puppeteer. Lo usas desde Rust, Node.js, TypeScript, Python o cualquier otro lenguaje.
Funciona con Node.js 18 o más reciente, y los tipos de TypeScript vienen incluidos.
Sistema
Arquitecturas
Windows
x64
macOS
Intel y ARM
Linux con glibc 2.35 o más reciente (Ubuntu 22.04, Debian 12)
x64 y ARM64
El paquete trae los binarios de las cinco plataformas, así que la descarga ronda los 40 MB. En Alpine y otras distribuciones con musl, usa el servidor con soblendwa-client.
No tiene dependencias y funciona en Node.js 18+, Bun, Deno y el navegador. En Node.js 22+, Bun y Deno usa el WebSocket incluido; en Node.js 18 a 21 instala también ws. Necesita un soblendwa-server corriendo.
import { connect } from 'soblendwa-client'
const bot = await connect('ws://127.0.0.1:8787', { session: 'bot', token: 'una-clave-larga' })
bot.on('qr', ({ code }) => console.log('Escanea este QR:', code))
bot.on('message', async (message) => {
if (message.info.fromMe) return
if (message.text === '!ping') {
const ms = await bot.elapsed(message)
await bot.reply(message, `Pong\n${ms.toFixed(2)} ms`)
}
})
Si usas el cliente JS, primero arranca el servidor con soblendwa-server --token "una-clave-larga" --session bot. El servidor dibuja el QR en la terminal.
El texto del evento qr es el contenido del código QR, no una imagen. Conviértelo en imagen con cualquier librería de QR, como qrcode-terminal en Node.js o qrcode en Python, o usa un código de 8 caracteres.
Ejecuta el bot.
En el teléfono, abre WhatsApp > Dispositivos vinculados > Vincular un dispositivo.
Escanea el QR. El bot se reinicia solo una vez y emite connected.
Desde otro número, envía !ping a la cuenta vinculada.
Tu bot abrió un WebSocket con WhatsApp, verificó su certificado y se registró como un dispositivo vinculado más, igual que WhatsApp Web. Las claves se guardaron en la carpeta o base de datos que elegiste, así que la próxima vez conectará sin QR.
Todo lo que ocurre llega como un evento. Tu bot reacciona a los que le interesan y llama métodos para actuar.
Después de connect(), si la sesión no está vinculada, llega un evento Qr con el contenido del código. El primero dura 60 segundos y los siguientes 20. Mientras no se escanee, llegan códigos nuevos. Si se agotan, la conexión termina con el motivo QrTimeout y, con la reconexión automática activada, empieza otra tanda de códigos.
Sirve cuando no puedes escanear, por ejemplo en un servidor sin pantalla. Pide el código después del primer evento qr y escríbelo en Dispositivos vinculados > Vincular con el número de teléfono.
if let Event::Qr(_) = event {
let code = client.request_pair_code("5215512345678").await?;
println!("Escribe este código en tu teléfono: {code}");
}
bot.once('qr', async () => {
const code = await bot.requestPairCode('5215512345678')
console.log('Escribe este código en tu teléfono:', code)
})
La sesión queda en el almacenamiento que elegiste y sobrevive a los reinicios. Termina solo si cierras la vinculación desde el teléfono, si llamas a logout() o si WhatsApp la invalida. En esos casos llega LoggedOut { reason }, los datos se borran y el siguiente connect() muestra un QR nuevo.
SoblendWA es un dispositivo más de tu cuenta, como WhatsApp Web. No se registra como teléfono: necesita un teléfono real que apruebe la vinculación, con WhatsApp instalado.
Cada cuenta, grupo o canal tiene un JID con la forma usuario:dispositivo@servidor.
Ejemplo
Qué es
Cómo crearlo en Rust
5215512345678@s.whatsapp.net
Una cuenta, por su número
Jid::phone("5215512345678")
5215512345678:3@s.whatsapp.net
El dispositivo 3 de esa cuenta
jid.with_device(3)
100000000000001@lid
Una cuenta, por su LID
Jid::lid("100000000000001")
120363000000000001@g.us
Un grupo
Jid::group("120363000000000001")
120363000000000001@newsletter
Un canal
Jid::new("120363000000000001", "newsletter")
status@broadcast
Los estados
Jid::status_broadcast()
Un JID también se puede leer desde un texto:
let jid: Jid = "5215512345678@s.whatsapp.net".parse()?;
let same = jid.same_user(&Jid::phone("52 1 55 1234 5678"));
Jid::phone conserva solo los dígitos, así que acepta números con espacios o signos. En Node.js, Python y el servidor, cualquier parámetro chat acepta un JID completo o solo el número.
WhatsApp identifica a muchas cuentas con un LID, un identificador que no revela el teléfono, sobre todo en grupos. Una misma persona puede aparecer por su número o por su LID.
SoblendWA resuelve ambos por ti. Cada mensaje trae info.sender_phone e info.sender_lid, y group.is_admin(&sender) funciona con cualquiera de los dos. Si WhatsApp no comparte el número de alguien, sender_phone queda vacío; el LID sigue sirviendo para identificarlo. Puedes consultar la relación con client.lid_for_phone y client.phone_for_lid.
info.chat es la conversación y info.sender es quien escribió. En un chat privado suelen coincidir; en un grupo, chat es el grupo y sender es la persona. Responde siempre a chat.
Cada mensaje tiene un id de texto. Para editarlo, borrarlo o fijarlo necesitas su clave (MessageKey): el chat, el id, si es tuyo y, en grupos, quién lo envió. msg.key() te la da hecha.
WhatsApp mete los mensajes dentro de otros: ephemeral, viewOnce, documentWithCaption, hijos de álbum, Meta AI, estados de grupo. SoblendWA los desenvuelve, así que msg.text(), msg.message.media() y msg.quoted() funcionan igual en todos. Si necesitas el original, está en msg.raw.
Todo lo que ocurre llega por un único canal de eventos: mensajes, recibos, cambios de grupo, desconexiones. En Rust es un enum, y en los demás lenguajes es JSON con un campo type. La lista completa está en Eventos.
Los chats distintos se atienden en paralelo y cada chat conserva su orden, al recibir y al enviar. Un texto nunca espera detrás de un video pesado de otro chat, y en el mismo chat los mensajes salen en el orden en que los pediste.
Una sesión es una cuenta vinculada con sus claves, guardadas en un store. Puedes tener varias en el mismo proceso, cada una con su propio cliente. El servidor maneja varias a la vez por nombre.
Mensajes
Enviar mensajes
Texto, respuestas, menciones, reacciones, ediciones y todo lo que no sea multimedia.
Todos los envíos devuelven el id del mensaje. Los ejemplos usan client en Rust y bot en los demás lenguajes.
Una mención necesita dos cosas: el @número en el texto y el JID en la lista de mencionados.
use soblendwa::Message;
let usuario = Jid::phone("5215511111111");
client.send_message(&chat, Message::text("hola @5215511111111").mention([&usuario])).await?;
client.send_message(&grupo, Message::text("Atención a todos").mention_all()).await?;
await bot.sendMentionAll(grupo, 'Atención a todos')
await bot.send_mention_all(grupo, "Atención a todos")
await bot.sendMentionAll(grupo, 'Atención a todos')
Message es el protobuf de WhatsApp con todos sus campos públicos, así que puedes construir cualquier mensaje. En otros lenguajes, sendProto acepta los bytes del protobuf.
let mensaje = Message { conversation: Some("Hecho a mano".into()), ..Default::default() };
client.send_message(&chat, mensaje).await?;
while let Some(event) = events.recv().await {
if let Event::Message(msg) = event {
if msg.info.from_me {
continue;
}
println!("{}: {:?}", msg.info.sender, msg.text());
}
}
bot.on('message', (message) => {
if (message.info.fromMe) return
console.log(message.info.sender, message.text)
})
@bot.on("message")
def show(message):
if not message["info"]["fromMe"]:
print(message["info"]["sender"], message["text"])
bot.on('message', (message) => {
if (message.info.fromMe) return
console.log(message.info.sender, message.text)
})
También llegan tus propios mensajes, enviados desde el teléfono o desde el bot: revisa siempre from_me.
Si un mensaje no se puede descifrar llega Event::UndecryptableMessage { info, error }, y SoblendWA pide un reintento al remitente y una copia a tu teléfono. Si después llega bien, recibes Event::Message normal y Event::MessageRecovered { id, chat, sender }. Los que siguen esperando se cuentan en stats().awaiting_recovery.
En Node.js, Python y el servidor, reply, react, download y otros aceptan el mensaje o su id. El id se busca entre los últimos 4.096 mensajes recibidos y, si activaste el store de chats, también ahí. Si es más viejo, recibes el error message … is not among the recent messages.
Mensajes
Multimedia
Imágenes, video, audio, documentos y stickers, desde unos kilobytes hasta archivos enormes.
with open("foto.jpg", "rb") as photo:
await bot.send_image(chat, photo.read(), caption="mi foto")
with open("nota.ogg", "rb") as audio:
await bot.send_audio(chat, audio.read(), voice_note=True)
with open("reporte.pdf", "rb") as document:
await bot.send_document(chat, document.read(), file_name="reporte.pdf", mimetype="application/pdf")
let a = client.prepare_media(&std::fs::read("a.jpg")?, MediaKind::Image, MediaOptions::default()).await?;
let b = client.prepare_media(&std::fs::read("b.jpg")?, MediaKind::Image, MediaOptions::default()).await?;
let ids = client.send_album(&chat, vec![a, b]).await?;
El primer id de la lista es el del álbum y los siguientes, los de cada foto o video.
if message["media"]:
data = await bot.download(message)
with open(message["info"]["id"] + ".bin", "wb") as output:
output.write(data)
if (message.media) {
const bytes = await bot.download(message)
}
La descarga verifica el HMAC y los dos SHA-256 antes de entregarte los bytes. Si el archivo ya caducó en el servidor, download_event y download_event_to_file le piden al remitente que lo vuelva a subir, y lo descargan de nuevo. Experimental
send_media_file lee el archivo dos veces por bloques de 256 KB: una para calcular los hashes que WhatsApp pide antes de subir y otra para cifrar mientras sube. No crea archivos temporales. El tipo MIME y el nombre del documento se deducen de la extensión.
download_to_file escribe en archivo.part y solo lo renombra cuando la verificación pasa, así que un archivo corrupto nunca aparece con su nombre final.
Método
Cuándo usarlo
upload_file
Subir un archivo del disco sin enviarlo todavía
upload_reader
Fuentes que solo se leen una vez, como un socket. Cifra hacia un archivo temporal y lo borra al terminar
Si envías el mismo archivo otra vez, como un sticker o la imagen de un comando, no se vuelve a cifrar ni a subir: sale tan rápido como un texto. En una cuenta real, una imagen repetida pasó de 1.420 ms a 4 ms.
El archivo se recuerda 7 días. Lo cambias con .media_cache(Some(duración)) o lo desactivas con .media_cache(None).
Pasada una hora se comprueba en el CDN que sigue disponible. Experimental
client.forget_upload(kind, sha256) olvida un archivo concreto.
stats().media_uploaded y stats().media_reused muestran cuánto ahorras.
El módulo soblendwa::imaging tiene las funciones que SoblendWA usa por dentro.
use soblendwa::imaging::{image_dimensions, profile_picture, thumbnail};
let datos = std::fs::read("foto.jpg")?;
let tamano = image_dimensions(&datos);
let miniatura = thumbnail(&datos, 100)?;
let perfil = profile_picture(&datos)?;
Mensajes
Vista previa de enlaces
Si un texto trae un enlace, la vista previa se genera sola, como en la app oficial.
Incluye título, descripción, miniatura y, si la página tiene video, la marca de video. No tienes que llamar a nada distinto.
use std::time::Duration;
use soblendwa::LinkPreviewConfig;
let config = LinkPreviewConfig { timeout: Duration::from_secs(2), enabled: true, ..Default::default() };
let (client, events) = Client::builder().store(store).link_previews(config).build().await?;
La consulta sale de tu servidor. Para generar la vista previa, el bot abre la página, como hace el teléfono en la app oficial. Si repite enlaces de desconocidos y no quieres eso, usa enabled: false.
Protegido contra enlaces maliciosos. Nunca consulta hosts privados: 127.0.0.1, la red interna, el servicio de metadatos de la nube (169.254.169.254), localhost, *.local ni *.internal. También revisa cada redirección y a qué IP resuelve el dominio, así un dominio público que apunte a tu red interna tampoco pasa.
Dos miniaturas. Una pequeña dentro del mensaje y otra nítida de hasta 1200 px, subida cifrada. Si la subida falla, el mensaje sale igual con la pequeña. La nítida aún es Experimental
Reutiliza lo que ya hizo. Un enlace repetido no vuelve a consultar la página, por 1 hora, ni a subir la miniatura.
Nunca frena un envío. Si la página falla o no responde, sale una tarjeta mínima con el dominio.
Imágenes hostiles. La imagen se decodifica con límites de tamaño y memoria.
Cambia el ajuste de privacidad de vistas previas de tu cuenta
message.link_thumbnail()
Datos para descargar la miniatura nítida de un mensaje recibido
Los mensajes recibidos traen en JSON linkPreview con url, title, description, video, hasThumbnail, hasHighQualityThumbnail, thumbnailWidth y thumbnailHeight.
Mensajes
Encuestas y eventos
Crea encuestas y eventos de calendario, y lee los votos y las respuestas ya descifrados.
El último número es cuántas opciones se pueden elegir. Con 1 se usa el formato de opción única; con 0 o más de una, el de varias opciones, igual que la app oficial.
let encuesta = client.send_message(&chat, Message::poll("¿Pizza o tacos?", ["Pizza", "Tacos"], 1)).await?;
client.send_message(&chat, Message::poll("¿Qué te gusta?", ["Rust", "Go", "Zig"], 0)).await?;
const encuesta = await bot.sendPoll(chat, '¿Pizza o tacos?', ['Pizza', 'Tacos'], 1)
await bot.sendPoll(chat, '¿Qué te gusta?', ['Rust', 'Go', 'Zig'], 0)
encuesta = await bot.send_poll(chat, "¿Pizza o tacos?", ["Pizza", "Tacos"], 1)
await bot.send_poll(chat, "¿Qué te gusta?", ["Rust", "Go", "Zig"], 0)
const encuesta = await bot.sendPoll(chat, '¿Pizza o tacos?', ['Pizza', 'Tacos'], 1)
await bot.sendPoll(chat, '¿Qué te gusta?', ['Rust', 'Go', 'Zig'], 0)
Message::announcement_poll es para grupos de anuncios de comunidades.
use soblendwa::events::EventResponseKind;
let evento = Message::event("Lanzamiento SoblendWA", 1_800_000_000)
.event_details(Some("En vivo"), Some(1_800_003_600), Some((19.4326, -99.1332, "CDMX")), None, true);
let id = client.send_message(&chat, evento).await?;
client.respond_event(&chat, &id, EventResponseKind::Going, 0).await?;
Los parámetros de event_details son: descripción, hora de fin en segundos Unix, lugar como (latitud, longitud, nombre), enlace para unirse y si se permiten invitados extra. .cancel_event() marca el evento como cancelado.
let info = client.group_info(&grupo).await?;
let actualizado = client.group_metadata(&grupo).await?;
let todos = client.joined_groups().await?;
const info = await bot.groupInfo(grupo)
const todos = await bot.joinedGroups()
info = await bot.group_info(grupo)
todos = await bot.joined_groups()
const info = await bot.groupInfo(grupo)
const todos = await bot.joinedGroups()
group_info responde desde la caché al instante, y group_metadata siempre consulta al servidor. La caché se actualiza sola cuando WhatsApp avisa de un cambio.
use soblendwa::ParticipantAction;
let nuevo = client.create_group("Equipo Soblend", &[Jid::phone("5215511111111")]).await?;
let resultados = client
.update_participants(&nuevo.jid, &[Jid::phone("5215522222222")], ParticipantAction::Promote)
.await?;
client.set_group_subject(&nuevo.jid, "Equipo").await?;
client.set_group_description(&nuevo.jid, Some("Hecho con SoblendWA")).await?;
client.set_group_picture(&nuevo.jid, std::fs::read("grupo.jpg")?).await?;
client.leave_group(&nuevo.jid).await?;
ParticipantAction puede ser Add, Remove, Promote o Demote. Cada resultado trae el jid y un status, parecido a un código HTTP: 200 significa que funcionó.
let codigo = client.group_invite_code(&grupo).await?;
println!("https://chat.whatsapp.com/{codigo}");
let nuevo_codigo = client.revoke_group_invite(&grupo).await?;
let detalles = client.group_invite_info(&nuevo_codigo).await?;
let unido = client.join_group(&nuevo_codigo).await?;
accept_group_invite(remitente, invitación) acepta una tarjeta de invitación recibida por mensaje. Para enviar una, usa send_group_invite.
Lista con jid, phone_number, lid, is_admin e is_super_admin
Cuentas y chats
Comunidades
Crear comunidades, vincular grupos y gestionar miembros. Experimental
let comunidad = client.create_community("Soblend", "Comunidad oficial").await?;
let subgrupo = client.create_community_group(&comunidad.jid, "Anuncios técnicos", &[]).await?;
let subgrupos = client.community_groups(&comunidad.jid).await?;
client.link_group(&comunidad.jid, &grupo).await?;
client.unlink_group(&comunidad.jid, &grupo).await?;
client.set_community_join_approval(&comunidad.jid, true).await?;
client.leave_community(&comunidad.jid).await?;
Una comunidad es un tipo de grupo, así que también funcionan las funciones de grupo, como group_info y update_participants. En GroupInfo, is_community indica si lo es, is_community_announce si es su grupo de anuncios y linked_parent a qué comunidad pertenece.
community_groups devuelve una lista de LinkedGroup con jid, subject, owner, created y size.
Para encuestas en grupos de anuncios, usa Message::announcement_poll.
let otro = client.newsletter_info_by_invite("https://whatsapp.com/channel/0029Va...").await?;
let mismo = client.newsletter_info(&otro.jid).await?;
client.newsletter_follow(&otro.jid).await?;
client.newsletter_mute(&otro.jid, true).await?;
client.newsletter_unfollow(&otro.jid).await?;
for publicacion in client.newsletter_messages(&otro.jid, 20, None).await? {
client.newsletter_react(&otro.jid, &publicacion.server_id, Some("🔥")).await?;
}
Los mensajes de canal que recibes traen msg.info.server_id, que es lo que usa newsletter_react. None quita tu reacción. newsletter_live_updates pide recibir en vivo los cambios del canal, y devuelve por cuántos segundos si WhatsApp lo indica.
Lo mismo que cambias en el teléfono: archivar, fijar, silenciar, etiquetas y contactos.
Estas funciones usan el app state de WhatsApp, la lista de cambios que se sincroniza entre tus dispositivos. Cada parche se verifica con LTHash y MACs antes de aplicarse. Si falta una clave, se le pide al teléfono y la sincronización continúa sola cuando llega.
Lo que cambies en el teléfono llega como Event::ChatUpdate { update, timestamp, initial_sync }. initial_sync indica si viene de la sincronización inicial.
use soblendwa::events::ChatUpdate;
if let Event::ChatUpdate { update: ChatUpdate::Archived { chat, archived }, .. } = event {
println!("{chat} archivado: {archived}");
}
ChatUpdate
En JSON
Campos
Muted
muted
chat, until
Archived
archived
chat, archived
Pinned
pinned
chat, pinned
MarkedRead
markedRead
chat, read
Locked
locked
chat, locked
Deleted, Cleared
deleted, cleared
chat
Starred
starred
chat, message_id, from_me, starred
DeletedForMe
deletedForMe
chat, message_id, from_me
Contact
contact
jid, full_name, first_name, lid, phone
ContactRemoved
contactRemoved
jid
PushName
pushName
name
Label
label
id, name, color, deleted
ChatLabel
chatLabel
label, chat, labeled
MessageLabel
messageLabel
label, chat, message_id, labeled
Other
other
index
En JSON, los campos usan camelCase, como messageId o fullName.
Normalmente no hace falta. client.sync_app_state(&COLLECTIONS, false) fuerza una sincronización, y COLLECTIONS contiene las colecciones que usa WhatsApp.
Cuentas y chats
Store de chats e historial
Una base local con tus chats, contactos y mensajes, que se alimenta sola.
use soblendwa::ChatStore;
let chats = ChatStore::open("session/chats.json").await?;
let (client, events) = Client::builder().store(store).chat_store(chats.clone()).build().await?;
for chat in chats.chats() {
println!("{} · {} sin leer", chats.name_of(&chat.jid).unwrap_or(chat.jid.to_string()), chat.unread);
}
let ultimos = chats.messages(&chat, 20);
let encontrados = chats.search("factura", 10);
let viejo = chats.message(&chat, "3EB0ABC...").unwrap();
client.reply(&viejo.to_event(), "respondiendo a un mensaje de ayer").await?;
Se alimenta solo con mensajes entrantes y enviados, historial, recibos (enviado, entregado, leído, reproducido), reacciones, votos de encuestas, ediciones, borrados, cambios de app state, nombres de contactos y asuntos de grupos.
Guarda hasta 500 mensajes por chat, ajustable con ChatStore::open_with_limit. Se escribe a disco cada 10 segundos si hubo cambios, y al llamar client.disconnect(). La escritura es atómica, así que un corte de luz no deja el archivo a medias.
Al vincular, el teléfono envía el historial en bloques, y cada uno llega como Event::HistorySync. También puedes pedir más.
let oldest = msg.key();
client.request_history(&chat, &oldest, msg.info.timestamp * 1000, 50).await?;
Pide entre 1 y 50 mensajes anteriores al indicado. El resultado llega como otro Event::HistorySync. request_placeholder_resend le pide al teléfono una copia de mensajes concretos que no se pudieron descifrar, y download_history descarga un bloque a partir de su notificación.
En JSON, historySync trae syncType, progress, chunkOrder, conversations, pushNames y lidMappings.
Cuentas y chats
Perfil y privacidad
Foto, información, bloqueos, ajustes de privacidad y mensajes temporales.
let foto = client.profile_picture_url(&chat, true).await?;
client.set_profile_picture(None, std::fs::read("yo.jpg")?).await?;
client.remove_profile_picture(None).await?;
Con None cambias la tuya; con Some(&grupo) la de un grupo. La imagen puede ser JPEG, PNG, WebP o GIF de cualquier tamaño: se recorta al centro y se deja en 640×640. En Node.js y Python: bot.profilePicture(jid, fullSize) y bot.profile_picture(jid, full_size).
use soblendwa::{PrivacySetting, PrivacyValue};
client.set_privacy(PrivacySetting::LastSeen, PrivacyValue::Contacts).await?;
let ajustes = client.privacy_settings().await?;
PrivacySetting
Qué controla
LastSeen
Última vez
Online
En línea
ProfilePicture
Foto de perfil
About
Información
ReadReceipts
Confirmaciones de lectura
GroupsAdd
Quién te añade a grupos
Calls
Quién te llama
Messages
Quién te escribe
PrivacyValue puede ser Everyone, Contacts, ContactsExcept, Nobody, MatchLastSeen o Known. No todos los valores valen para todos los ajustes: WhatsApp rechaza las combinaciones que no admite.
Los cambios llegan como Event::PrivacyChanged y Event::BlocklistChanged, y los de otras personas como Event::PictureChanged y Event::AboutChanged. Experimental
let encontrados = client.on_whatsapp(&["5215511111111"]).await?;
let lid = client.lid_for_phone(&Jid::phone("5215511111111")).await?;
let telefono = client.phone_for_lid(&Jid::lid("100000000000001")).await?;
Para saber si alguien está en línea, suscríbete y atiende Event::Presence { from, available, last_seen }. Con .online_on_connect(false) el bot no aparece en línea al conectar.
use soblendwa::events::CallKind;
if let Event::Call(llamada) = event {
if llamada.kind == CallKind::Offer {
client.reject_call(&llamada.id, &llamada.from).await?;
}
}
let enlace = client.create_call_link(true, None).await?;
Event::Call trae id, from, kind (Offer, Accept, Reject o Terminate), video, is_group y timestamp. En create_call_link, true significa video, y el segundo parámetro es una hora de inicio opcional en segundos Unix.
Cuentas y chats
Catálogo de empresa
Productos, colecciones, pedidos y el perfil de tu cuenta de empresa. Experimental
use soblendwa::business::{CatalogImage, ProductDraft};
let pagina = client.catalog(None, 10, None).await?;
let producto = client.product_create(&ProductDraft {
name: Some("Taza Soblend".into()),
price: Some(149_000),
currency: Some("MXN".into()),
images: vec![CatalogImage::Data(std::fs::read("taza.jpg")?)],
..Default::default()
}).await?;
client.product_update(&producto.id, &ProductDraft { hidden: Some(true), ..Default::default() }).await?;
client.product_delete(&[&producto.id]).await?;
Los precios van en milésimas, como en la app: 149_000 son 149,00. Las imágenes pueden ser bytes (CatalogImage::Data) o una URL (CatalogImage::Url). Para seguir paginando, pasa next_cursor como tercer parámetro de catalog.
La espera crece y tiene un margen aleatorio, para que cien sesiones no se reconecten en el mismo milisegundo.
Fallo seguido
Espera
Primero
De 0 a 0,75 s
Segundo
1 s más un margen aleatorio
Tercero
2 s más un margen aleatorio
Siguientes
4 s, 8 s… hasta 60 s, más un margen aleatorio
Se apaga con .auto_reconnect(false). El keepalive envía un ping cada 15 segundos y, si no hay respuesta en 8, reconecta. El servidor además escalona el arranque de las sesiones.
Un bug que manda mensajes en bucle no se convierte en spam. Lo que pase el límite devuelve Error::RateLimited { chat, retry_after } y se cuenta en stats().rate_limited.
Campo de SendLimit
Por defecto
Qué limita
per_minute
240
Mensajes por minuto, en total
burst
60
Ráfaga máxima, en total
per_chat_per_minute
40
Mensajes por minuto, en cada chat
per_chat_burst
15
Ráfaga máxima, en cada chat
use soblendwa::SendLimit;
let limite = SendLimit { per_minute: 120, burst: 30, per_chat_per_minute: 20, per_chat_burst: 8 };
let (client, events) = Client::builder().store(store).send_limit(Some(limite)).build().await?;
.send_limit(None) lo desactiva. Bajar los límites reduce el riesgo, pero no hay ningún ritmo que garantice que WhatsApp no bloquee una cuenta.
Cada mensaje se guarda en un outbox hasta que WhatsApp confirma que lo recibió. Si se cae la conexión o reinicias el proceso, se reenvía al reconectar con el mismo id, y un mensaje que sigue en vuelo no se reenvía.
client.pending_sends() muestra lo que está esperando: id, chat, queued_at y attempts.
Si un handler entra en pánico, se registra, se cuenta en stats().panics y la sesión sigue. Cada sesión del servidor es independiente: si una se cuelga abriendo su almacenamiento, las demás siguen.
Si un mensaje no se puede descifrar, SoblendWA pide un reintento al remitente y una copia a tu teléfono a la vez. Cuando llega bien, emite Event::MessageRecovered. Esto está probado de punta a punta contra el WhatsApp falso, y aún falta confirmarlo con una cuenta real. Experimental
let bot = msg.info.elapsed();
let red = client.latency();
let medido_ahora = client.ping().await?;
msg.info.elapsed() mide cuánto lleva el mensaje dentro del bot, desde que llegó del socket. Incluye descifrarlo.
client.latency() devuelve al instante la latencia de red medida en segundo plano.
client.ping() hace una medición nueva y tarda un viaje de ida y vuelta.
La primera vez que el bot le escribe a un chat nuevo, tiene que pedir los dispositivos del destinatario y sus claves: uno o dos viajes de ida y vuelta, igual que WhatsApp Web. Después todo queda guardado y los envíos solo cuestan el cifrado local.
El tiempo que ves en el teléfono es la suma del viaje de tu teléfono a WhatsApp, unos 0,05 ms del bot, y el viaje de WhatsApp al bot y de vuelta. Esos viajes dependen de la distancia física y de tu conexión, y ningún código puede bajarlos de lo que permita la red.
Operación
Rendimiento
Lo que se midió, cómo se midió y qué significa. Las pruebas son de laboratorio, y en producción manda la red.
Un bot de eco de punta a punta. Los dos se conectan al mismo @zapo-js/fake-server con el protocolo real (Noise, vinculación por QR, Signal), cada uno en su propio proceso y con almacenamiento en memoria. zapo corre con su acelerador @zapo-js/native.
Mediana de 3 corridas alternadas (500 idas y vueltas + 500 en ráfaga)
Codificar + decodificar un stanza de 21 nodos (3.375 bytes)
63.242 stanzas/s
12.810 (4,9× menos)
5.320 (11,9× menos)
Cifrar + descifrar un mensaje 1:1 (Signal)
135.504 msg/s
12.223 (11× menos)
3.720 (36× menos)
Cifrar + descifrar un mensaje de grupo (Sender Keys)
9.117 msg/s
3.565 (2,6× menos)
58 (157× menos)
Se midió en la misma máquina contra el código real de cada librería: whatsmeow del 2 de noviembre de 2026 con su libsignal v0.2.2 (Go 1.27.2) y Baileys 7.0.0-rc14 con su libsignal y su GroupCipher. Mismo stanza, mismo mensaje de 120 bytes y mismas operaciones; las tres librerías producen exactamente los mismos 3.375 bytes al codificar. SoblendWA serializa la sesión en cada mensaje; a whatsmeow y Baileys se les midió con los registros como objetos en memoria, que es su caso más rápido. whatsmeow serializando como su almacén SQL baja a 6.773 msg/s.
Desde que llega un mensaje hasta que sale la respuesta, el bot tarda unos 0,05 ms en un chat conocido.
Sin esperas de red en chats conocidos. Las sesiones, los dispositivos y los datos de los grupos se guardan en disco y se usan al instante, aunque reinicies el bot. Se actualizan en segundo plano y cuando WhatsApp avisa de un cambio. Al conectar se precargan tus grupos y los dispositivos de sus miembros.
Grupos grandes rápidos desde el primer mensaje. Las consultas de claves y dispositivos salen en paralelo, en un solo viaje de red, y las sesiones Signal se crean usando todos los núcleos: 1000 dispositivos en unos 0,1 s en lugar de 0,4 s.
Socket sin demoras.TCP_NODELAY evita que el sistema retenga mensajes pequeños. El lector y el escritor tienen cada uno su propio cifrador AES-GCM y no comparten locks.
Arranque rápido. La versión de WhatsApp Web se recuerda y se actualiza en segundo plano.
Almacenamiento sin bloqueos.FileStore escribe en un log de solo-escritura únicamente lo que cambió y compacta de vez en cuando. Los backends SQL y Redis agrupan las escrituras en segundo plano.
Colas por chat. Los chats distintos van en paralelo y cada chat conserva su orden.
Multimedia reutilizada. El mismo archivo no se vuelve a cifrar ni a subir.
Sin código inseguro.#![forbid(unsafe_code)], y el decodificador binario está probado contra cientos de miles de entradas hostiles.
Operación
Almacenamiento
Dónde se guardan las claves y el estado de cada cuenta.
use std::sync::Arc;
use soblendwa::store::{MongoStore, PostgresStore, RedisStore, SqliteStore};
use soblendwa::{FileStore, MemoryStore};
let archivos = FileStore::open("session").await?;
let memoria = Arc::new(MemoryStore::new());
let sqlite = Arc::new(SqliteStore::open_account("datos/bots.db", "ventas").await?);
let postgres = Arc::new(PostgresStore::connect_account("postgres://bot:clave@localhost/wa", "ventas").await?);
let redis = Arc::new(RedisStore::connect_account("redis://127.0.0.1/", "ventas").await?);
let mongo = Arc::new(MongoStore::connect_account("mongodb://localhost:27017", "wa", "ventas").await?);
let (client, events) = Client::builder().store(sqlite).build().await?;
account permite guardar varias cuentas en la misma base. Con FileStore, usa una carpeta por cuenta. Si no indicas un store, Rust usa memoria, y Node.js y Python usan la carpeta session.
Las credenciales y las claves de la cuenta son secretos. Protege el acceso a tu base de datos y a tu carpeta de sesión como protegerías una contraseña.
Todos los backends de Rust comparten el mismo núcleo, BatchedStore: las lecturas salen de memoria después de la primera vez, y las escrituras se agrupan en una sola transacción, o un solo pipeline en Redis, en segundo plano. Si una clave cambia mil veces antes de escribirse, solo se escribe el último valor. Enviar un mensaje nunca espera a la base de datos. client.disconnect() vacía lo pendiente antes de salir.
Con el módulo de Node.js pasas un objeto con tus funciones y usas la base de datos que quieras. Las lecturas ya pasan por caché y las escrituras llegan agrupadas, así que no hace falta optimizar nada.
Lista de claves. Opcional, se usa al exportar a zapo
apply(writes)
Lista de { op, kind, key, value }, con op igual a set, remove o clear
—
En lugar de apply puedes dar set(kind, key, value), remove(kind, key) y clear(). Probado de punta a punta: vinculación, mensajes y reinicio con el mismo store.
Implementa Backend, con cuatro métodos más list si quieres exportar a zapo, y envuélvelo en BatchedStore::new(tu_backend): la caché y el agrupado vienen gratis.
Si prefieres controlarlo todo, implementa directamente el trait Store, con load_creds, save_creds, get, set, remove, clear, y opcionalmente flush y keys.
Las opciones se pasan a SoblendWA.create. En Python usan snake_case, y se convierten solas.
JavaScript
Python
Rust
store
store
store
chatStore: { path, limit }
chat_store={...}
chat_store
pushName
push_name
push_name
deviceName
device_name
device_name
platform
platform
platform
countryCode
country_code
country_code
onlineOnConnect
online_on_connect
online_on_connect
autoReconnect
auto_reconnect
auto_reconnect
proxy
proxy
proxy
linkPreviews
link_previews
link_previews
server: { wsUrl, noiseRootHex, mediaEndpoint }
server={...}
ws_url, trust_root, media_endpoint
send_limit, outbox, media_cache, workers y query_timeout solo se pueden cambiar desde Rust por ahora. En los demás lenguajes se usan los valores por defecto.
SoblendWA escribe sus registros con tracing. En Rust, instala un suscriptor como tracing-subscriber. El servidor los muestra por defecto, y se ajustan con la variable RUST_LOG.
use soblendwa::Proxy;
let proxy = Proxy::parse("socks5://usuario:clave@127.0.0.1:1080")?;
let (client, events) = Client::builder().store(store).proxy(proxy).build().await?;
El usuario y la contraseña son opcionales. Pasan por el proxy el WebSocket, las subidas y descargas de multimedia, las vistas previas de enlaces y la consulta de la versión de WhatsApp Web. Se probó contra los servidores reales de WhatsApp a través de un SOCKS5 con usuario y contraseña.
Operación
Migrar desde otra librería
Trae tu sesión de zapo o de Baileys sin volver a escanear el QR.
Convierte las credenciales, las sesiones Signal, incluidas las claves de mensajes que llegaron fuera de orden, las pre-keys, las sender keys de grupos, las claves y versiones de app state y los pares número ↔ LID de los contactos. La base de zapo se abre solo para leer: no se modifica.
Probado contra zapo-js 1.11 real, en los dos sentidos: zapo crea la base, SoblendWA la importa, descifra los mensajes pendientes y responde; los contactos de zapo descifran esas respuestas; tras el rollback, zapo sigue mandando y recibiendo mensajes privados y de grupo.
La conversión se probó con documentos que siguen el esquema exacto de @zapo-js/store-mongo, y descifra mensajes pendientes reales de zapo, pero todavía no contra un servidor MongoDB. Haz una copia de la base antes del rollback. Desde código: ZapoMongo::connect, import_zapo_mongo y export_zapo_mongo. Si usas un prefijo de colecciones, indícalo con .collection_prefix(...).
cargo run --release --example import_baileys -- ./auth_info ./session
let store = FileStore::open("session").await?;
let reporte = soblendwa::store::import_baileys("auth_info", store.as_ref()).await?;
println!("{} sesiones, {} claves de grupo", reporte.sessions, reporte.sender_keys);
from soblendwa import import_baileys
report = await import_baileys("./auth_info", type="sqlite", path="session/auth.db")
Convierte las credenciales, las sesiones Signal de libsignal-node, las pre-keys, las sender keys de grupos, las claves y versiones de app state y los mapeos número ↔ LID. Las listas de dispositivos y los tokens se vuelven a pedir solos. El bot sigue vinculado, y los chats en curso siguen cifrándose con las mismas sesiones.
El reporte trae me, sessions, pre_keys, sender_keys, app_state_keys, app_state_versions, lid_mappings, skipped y failed. Revisa failed por si algo no se pudo convertir.
FakeWhatsApp (feature testing) es un servidor de WhatsApp en memoria que habla el protocolo real: handshake Noise con su propia cadena de certificados, nodos binarios, reparto de pre-keys, USync, grupos con sender keys, acks, recibos y mensajes guardados para cuentas desconectadas. Las cuentas son clientes SoblendWA normales, así que prueba tu bot completo, cifrado incluido.
use soblendwa::testing::FakeWhatsApp;
use soblendwa::Event;
let fake = FakeWhatsApp::start().await?;
let (bot, mut eventos) = fake.connect("5215511111111").await?;
let (ana, _) = fake.connect("5215522222222").await?;
ana.send_text(&FakeWhatsApp::jid("5215511111111"), "!ping").await?;
while let Some(evento) = eventos.recv().await {
if let Event::Message(msg) = evento {
assert_eq!(msg.text(), Some("!ping"));
break;
}
}
Simula también la subida y descarga de multimedia, y permite provocar fallos.
SoblendWA también pasa contra @zapo-js/fake-server: handshake Noise y certificado, vinculación por QR con firma ADV, mensajes privados y de grupo. Desde Node.js:
Se conecta a WhatsApp y espera el QR por WebSocket
node bindings/node/test.js
El módulo de Node.js
python bindings/python/test_bindings.py
El módulo de Python
node clients/js/test.mjs
El cliente JS. Usa SOBLENDWA_URL y SOBLENDWA_TOKEN, con un servidor corriendo
Incluye pruebas de robustez del decodificador binario: cientos de miles de entradas aleatorias y mutadas, anidamiento hostil y bombas de compresión. Ninguna entrada puede provocar un pánico, un desbordamiento de pila ni agotar la memoria.
El CI de GitHub corre formato, clippy y todas las pruebas en Linux, Windows y macOS, compila con Rust 1.89, y levanta el servidor con --fake para pasarle el harness desde JavaScript.
cargo run --release --example bot dibuja el QR en la terminal y responde unos 30 comandos, uno por función. Escribe !ayuda desde otro número. Si un comando falla, el error te llega como respuesta en el mismo chat.
Comando
Qué hace
!ping
Responde Pong y el tiempo que tardó el bot
!prueba
Revisa de una vez la latencia, los tiempos por fase, la reutilización de multimedia y el outbox
!stats
Muestra las métricas
!quien
Muestra el número y el LID de quien escribe
!outbox
Muestra lo que sigue pendiente
La sesión se guarda en ./session, o en la carpeta que le pases: --example bot -- mi-carpeta.
Manda texto y multimedia entre cuentas vinculadas durante días, y escribe cada minuto en soak.jsonl la memoria por proceso, los mensajes perdidos, los que no se pudieron descifrar, las reconexiones y las métricas de cada cuenta. Con --webhook avisa en cuanto aparece un mensaje sin descifrar o un bloqueo. Para medir la memoria por cuenta, corre un servidor por cuenta y pasa --pid cuenta=pid.
cargo run --release --example canary -- ./sesion-prueba-a ./sesion-prueba-b
Conecta dos cuentas de prueba, manda un texto y una imagen de una a otra, comprueba que se descifran y que la imagen descargada es idéntica, y revisa ping, mensajes sin descifrar y pánicos. Con SOBLENDWA_ALERT_WEBHOOK (Discord, Slack o cualquier webhook JSON) avisa si algo falla. Prográmalo una vez al día con cron, un timer de systemd o el Programador de tareas, para enterarte el mismo día si WhatsApp cambió algo.
Lenguajes
Node.js y TypeScript
El motor en Rust, cargado como módulo nativo dentro del proceso de tu bot.
El cifrado Signal, el códec binario, el socket y el almacenamiento corren en Rust. JavaScript solo recibe eventos y llama métodos, así que el bot no se frena por el recolector de basura ni por el hilo de eventos de Node.js.
Las opciones de create usan snake_case y se convierten solas. Están en Configuración. Las opciones anidadas también, como link_previews={"timeout_ms": 2000}.
@bot.on("tipo") registra un handler, que puede ser normal o async. "event" recibe todos. bot.run() conecta y atiende los eventos hasta que la sesión termine.
Si prefieres manejar el bucle tú mismo:
await bot.connect()
async for event in bot.events():
print(event["type"])
Los eventos son diccionarios con los mismos campos que en JSON, en camelCase, como message["info"]["fromMe"].
Si te desconectas del servidor, el cliente se reconecta solo, con una espera creciente de hasta 30 segundos. WhatsApp sigue conectado en el servidor mientras tanto, así que no se pierde la sesión.
Además de los eventos de WhatsApp, el cliente emite open, close, lagged y error. lagged avisa de que tu bot iba tan lento que el servidor descartó eventos.
Los mismos que el módulo de Node.js, con todo asíncrono, porque cada llamada viaja al servidor. También tiene status(), read(message), close() y call(método, parámetros) para cualquier otro.
sendMedia y compañía mandan los bytes por el WebSocket, en base64. sendFile y downloadToFile usan rutas del servidor y son la opción para archivos grandes. bytes puede ser un Uint8Array, un ArrayBuffer o un texto en base64.
Lenguajes
Servidor
soblendwa-server corre el motor y expone WhatsApp por WebSocket, HTTP y webhooks, para cualquier lenguaje.
Un solo servidor maneja varias cuentas a la vez, y usó unos 14 MB de RAM con dos cuentas contra el WhatsApp falso.
La primera vez muestra el QR de cada cuenta en la terminal. Para reutilizar una sesión que ya vinculaste con el bot de ejemplo, usa --dir . --session session.
Para llamar un método, envía un id tuyo, y la respuesta trae el mismo. Las llamadas se atienden en paralelo, así que las respuestas pueden llegar en otro orden.
Con --webhook, cada evento se envía por POST a tu URL, con el mismo JSON que en el WebSocket y un límite de 10 segundos. Si hay --token, va en la cabecera Authorization, y conviene que lo revises para saber que el aviso viene de tu servidor. Si la entrega falla, se registra en los logs y no se reintenta.
Son los mismos por WebSocket y por HTTP. chat acepta un JID o solo el número. messageId es el id de un mensaje reciente o guardado en el store de chats. Los bytes viajan en base64.
var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer mi-clave");
var body = new StringContent("{\"chat\":\"5215512345678\",\"text\":\"Hola\"}");
var response = await http.PostAsync("http://127.0.0.1:8787/api/ventas/sendText", body);
Referencia de la API
Client
Cada método público del cliente de Rust. En esta página, async indica que se usa con .await.
La página también está en el buscador: pulsa Ctrl K y escribe el nombre de un método.
WebSocket. Se abre un WSS hacia WhatsApp, a través del proxy si lo configuraste, con TCP_NODELAY.
Handshake Noise. Se negocia XX_25519_AESGCM_SHA256 y se verifica la cadena de certificados de WhatsApp y su vigencia. A partir de aquí todo va cifrado.
Inicio de sesión. Si no hay cuenta vinculada, se registra y llegan los QR. Si la hay, se presenta con sus claves.
Preparación. Se suben pre-keys si hacen falta, se sincroniza el app state, y se precargan tus grupos y los dispositivos de sus miembros.
Listo. Llega Event::Connected, y empiezan el keepalive y el vaciado del outbox.
Outbox. El mensaje se guarda, por si se cae la conexión.
Cola. Entra en la cola de su chat, para conservar el orden.
Vista previa. Si trae un enlace, se genera. Nunca frena el envío más de lo configurado.
Dispositivos. Se buscan los del destinatario, en caché, y se crean las sesiones Signal que falten. Es la única parte con esperas de red, y solo la primera vez.
Cifrado. Se cifra para cada dispositivo. En grupos, se usa la sender key.
Confirmación. WhatsApp responde, llega Event::ServerAck con sus tiempos, y el mensaje sale del outbox.
src/
├── binary/ códec de nodos binarios, diccionario de tokens y pruebas de robustez
├── crypto/ X25519, firmas XEdDSA, AES-GCM/CBC/CTR, HKDF, PBKDF2
├── noise.rs handshake Noise XX, verificación de certificados y framing
├── proto/ mensajes protobuf de WhatsApp y de Signal
├── signal/ sesiones (X3DH + Double Ratchet) y Sender Keys
├── store/ credenciales, FileStore, MemoryStore, BatchedStore,
│ SQLite, PostgreSQL, Redis, MongoDB, Baileys y zapo
├── chats.rs store de chats, contactos y mensajes
├── interactive.rs botones, listas, plantillas y mensajes interactivos
├── json.rs eventos, mensajes normalizados, métricas y nodos a JSON
├── testing.rs WhatsApp falso en memoria con el protocolo real
└── client/ conexión, vinculación, envío, recepción, app state, grupos,
comunidades, canales, catálogo, cuenta, multimedia, proxy,
presencia, tokens, USync, outbox, vista previa, límite de
envíos, colas por chat y métricas
bindings/
├── bridge/ sesión compartida y despachador de métodos por nombre
├── node/ módulo nativo para Node.js (napi-rs)
├── python/ módulo nativo para Python (PyO3, asyncio)
└── server/ servidor WebSocket + HTTP + webhooks (axum)
clients/
└── js/ cliente JavaScript/TypeScript del servidor
examples/ bot de pruebas, bot de eco, vinculación con código, canario
e importación de Baileys y de zapo
tests/fixtures/ sesiones con claves y números de prueba, para las migraciones
docs/ este sitio
Comando por comando con el bot de ejemplo: vinculación por QR, mensajes privados y de grupo, incluidos grupos con LID, !ping, editar, eliminar para todos, reacciones, fijar, contactos, vista previa de enlaces, encuestas de una y de varias opciones con votos descifrados, eventos de calendario, álbumes, documentos grandes subidos en streaming, descarga verificada a disco, mensajes interactivos, archivar, store de chats y búsqueda, bots de Meta AI, mensajes temporales, límite de contacto nuevo, canales, grupos e invitaciones, y confirmaciones del servidor.
También con una cuenta real y !prueba: los mensajes viejos no se ejecutan, con más de 1.000 al arrancar; número y LID resueltos; admin detectado con solo el LID; tiempos por fase; la imagen repetida sale sin volver a subirse, de 1.420 ms a 4 ms; y el outbox se vacía cuando WhatsApp confirma.
Probado contra los servidores de WhatsApp, sin teléfono Probado#
Conexión a través de un proxy SOCKS5 con usuario y contraseña, los módulos de Node.js y Python, y el servidor WebSocket con el cliente JavaScript, con el QR recibido por WebSocket y la API HTTP.
libsignal-node: una sesión creada por libsignal se importa, SoblendWA descifra mensajes pendientes y libsignal descifra las respuestas.
zapo-js 1.11: importación de su base SQLite y rollback, en los dos sentidos.
**@zapo-js/fake-server:** handshake Noise y certificado, vinculación por QR con firma ADV, mensajes privados y de grupo, y un store escrito en JavaScript que sobrevive a un reinicio.
Mensajes privados y de grupo entre cuentas, mensajes dañados o sin sender key que se recuperan con el reintento, reintentos después de reiniciar, reconexión con reenvío del outbox sin duplicar, cuenta bloqueada que se detiene, límite de envíos, multimedia subida y descargada, también en streaming, vista previa completa con bloqueo de hosts privados, eventos tipados, y el servidor con el cliente JavaScript y el harness de pruebas largas.
Las pruebas pasan en Linux, Windows y macOS, y el código compila con Rust 1.89. Se compilan 16 binarios para las distintas plataformas, y cada módulo de Node.js se carga en la suya. Los paquetes publicados se instalaron y ejecutaron en Windows; los de Linux y macOS no se han usado fuera del CI.
Implementado y probado solo localmente, todavía no contra una cuenta real:
Crear comunidades y canales.
Catálogo de empresa y mensajes de producto.
Re-subida de multimedia caducada.
Etiquetas, respuestas rápidas y etiquetas de miembro.
El reenvío del outbox tras una caída real.
La recuperación tras reintento con WhatsApp real.
El evento de cuenta bloqueada con un bloqueo real.
La reutilización de multimedia pasada la hora, con verificación en el CDN.
La miniatura nítida de la vista previa.
Los eventos de foto, información, bloqueos y privacidad.
El canario diario.
Sin probar contra un servidor real: MongoStore, PostgresStore, RedisStore y la importación desde @zapo-js/store-mongo. Tampoco se ha hecho una prueba de varios días en producción.
Si alguno falla con tu cuenta, abre un issue con el error.
SoblendWA es un proyecto independiente. No está afiliado, asociado ni respaldado por WhatsApp ni por Meta.
Automatizar una cuenta puede ir contra los Términos de Servicio de WhatsApp y provocar que la bloqueen. Úsalo con responsabilidad y no envíes spam. Ningún límite ni ajuste de la librería garantiza que una cuenta no sea bloqueada.