SoblendWA

Documentación de la versión 0.1

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.

cargo add soblendwa
cargo add tokio --features full

Elige por dónde empezar#

Rendimiento#

Un bot de eco contra el mismo servidor, comparado con zapo-js 1.11 con su acelerador activado.

4,2 msida y vuelta (p50), frente a 10,5 ms
0,7 msde CPU por mensaje, frente a 4,5 ms
12 MBde memoria, frente a 130 MB
100 mspara conectar y vincular, frente a 1.565 ms

Es una prueba de laboratorio sin red real, con el método y sus límites explicados en Rendimiento.

Antes de usarlo#

Empieza

Instalación

Elige el paquete según el lenguaje de tu bot. Todos usan el mismo motor en Rust.

Qué paquete necesitas#

Tu bot está en…InstalaSe ejecuta
Rustcargo add soblendwaDentro de tu programa
Node.js o TypeScriptnpm install soblendwa-nodeMódulo nativo, en el proceso de tu bot
Python 3.9+ con asynciopip install soblendwaMódulo nativo, fuera del GIL
JavaScript sin módulos nativos (Bun, Deno, navegador, Alpine)npm install soblendwa-client más soblendwa-serverUn servidor aparte
Go, PHP, Java, C# o cualquier otrosoblendwa-serverUn servidor aparte, por WebSocket, HTTP o webhooks

No necesitas Rust instalado para usar los paquetes de Node.js, Python ni los binarios del servidor.

Rust#

Necesitas Rust 1.89 o más reciente.

Terminal
cargo add soblendwa
cargo add tokio --features full

O en tu Cargo.toml, activando las features que quieras:

Cargo.toml
[dependencies]
soblendwa = { version = "0.1", features = ["sqlite"] }
tokio = { version = "1", features = ["full"] }
FeatureQué añade
sqliteSqliteStore, con SQLite ya incluido en el binario, y la importación desde zapo
postgresPostgresStore
redisRedisStore
mongoMongoStore y la importación desde @zapo-js/store-mongo Experimental
storesLas cuatro anteriores
testingsoblendwa::testing::FakeWhatsApp, el WhatsApp falso

Sin features ya tienes FileStore y MemoryStore.

Node.js y TypeScript#

Terminal
npm install soblendwa-node

Funciona con Node.js 18 o más reciente, y los tipos de TypeScript vienen incluidos.

SistemaArquitecturas
Windowsx64
macOSIntel 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.

Python#

Terminal
pip install soblendwa

Funciona con Python 3.9 o más reciente gracias al ABI estable, en Windows x64, macOS (Intel y ARM) y Linux x64 o ARM64 (manylinux 2.28).

Cliente JavaScript#

Terminal
npm install 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.

Servidor sin compilar#

Cada versión publica binarios en Releases. Los de Linux con musl son estáticos: funcionan en cualquier VPS sin instalar nada.

VPS Linux x64
curl -L -o soblendwa.tar.gz https://github.com/Soblend-Development-Studio/SoblendWA/releases/latest/download/soblendwa-server-x86_64-unknown-linux-musl.tar.gz
tar -xzf soblendwa.tar.gz
./soblendwa-server --token "una-clave-larga" --session bot
ArchivoPara
soblendwa-server-x86_64-unknown-linux-musl.tar.gzLinux x64, estático
soblendwa-server-aarch64-unknown-linux-musl.tar.gzLinux ARM64, estático
soblendwa-server-x86_64-unknown-linux-gnu.tar.gzLinux x64 con glibc
soblendwa-server-x86_64-pc-windows-msvc.zipWindows x64
soblendwa-server-aarch64-apple-darwin.tar.gzmacOS ARM
soblendwa-server-x86_64-apple-darwin.tar.gzmacOS Intel

También puedes compilarlo con Rust:

Terminal
cargo install --git https://github.com/Soblend-Development-Studio/SoblendWA soblendwa-server

Compilar los módulos desde el código#

Node.js
git clone https://github.com/Soblend-Development-Studio/SoblendWA
cd SoblendWA/bindings/node
npm install
npm run build
npm test
Python
cd SoblendWA/bindings/python
pip install maturin
maturin develop --release

Empieza

Inicio rápido

En cinco minutos tendrás un bot que responde a !ping en tu propio WhatsApp.

Lo que necesitas#

  • Un teléfono con WhatsApp, para escanear el QR. Usa una cuenta de pruebas si puedes.
  • Rust, Node.js o Python instalados, según el lenguaje que elijas.

1. Escribe el bot#

use soblendwa::{Client, Event, FileStore};

#[tokio::main]
async fn main() -> soblendwa::Result<()> {
    let store = FileStore::open("session").await?;
    let (client, mut events) = Client::builder()
        .store(store)
        .push_name("Mi Bot")
        .build()
        .await?;
    client.connect().await?;

    while let Some(event) = events.recv().await {
        match event {
            Event::Qr(code) => println!("Escanea este QR: {code}"),
            Event::Connected => println!("Conectado como {:?}", client.me()),
            Event::Message(msg) if !msg.info.from_me => {
                if msg.text() == Some("!ping") {
                    let ms = msg.info.elapsed().as_secs_f64() * 1000.0;
                    client.reply(&msg, format!("Pong\n{ms:.2} ms")).await?;
                }
            }
            Event::LoggedOut { reason } => {
                println!("Sesión cerrada: {reason}");
                break;
            }
            _ => {}
        }
    }
    Ok(())
}

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.

2. Vincula tu teléfono#

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.

  1. Ejecuta el bot.
  2. En el teléfono, abre WhatsApp > Dispositivos vinculados > Vincular un dispositivo.
  3. Escanea el QR. El bot se reinicia solo una vez y emite connected.
  4. Desde otro número, envía !ping a la cuenta vinculada.
  5. Recibes Pong y el tiempo que tardó el bot.

3. Qué acaba de pasar#

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.

Siguientes pasos#

Empieza

Vincular y sesión

Cómo se vincula una cuenta, cuánto dura la sesión y qué pasa cuando se corta la conexión.

Vincular con QR#

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.

Al escanearlo llegan, en este orden:

EventoQué significa
PairSuccess { id, lid, platform, business_name }El teléfono aprobó la vinculación
Disconnected { reason: RestartRequired, reconnecting: true }Reinicio normal que pide WhatsApp tras vincular. No es un error
ConnectedLa sesión está lista para enviar y recibir

Vincular con código#

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}");
}

Escribe el número con código de país y sin espacios. También hay un ejemplo listo: cargo run --release --example pair_code -- 5215512345678.

Cuánto dura la sesión#

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.

Motivos de desconexión#

Cada desconexión llega como Disconnected { reason, reconnecting }.

Motivo en RustEn JSONQué significa¿Reintenta?
ConnectionLost(texto)connectionLostSe cayó la red o el socketSí, si auto_reconnect está activo
RestartRequiredrestartRequiredWhatsApp pide reiniciar, normal tras vincularSí, siempre
ReplacedreplacedOtra conexión tomó esta sesiónNo
QrTimeoutqrTimeoutNadie escaneó a tiempoSí, si auto_reconnect está activo, con QR nuevos
ClientOutdatedclientOutdatedWhatsApp rechazó la versión del cliente (código 405)Sí, siempre, tras olvidar la versión guardada
AccountBlocked(código)accountBlockedCuenta bloqueadaNo
ManualmanualLlamaste a disconnect()No

La espera entre reintentos está explicada en Confiabilidad.

Cerrar la conexión#

MétodoQué hace¿Conserva la sesión?
client.disconnect()Cierra el socket y guarda lo pendiente en el almacenamientoSí
client.logout()Le pide a WhatsApp desvincular el dispositivo y borra los datosNo

Estado de la conexión#

MétodoDevuelve
client.is_connected()Si el socket está conectado y autenticado
client.is_logged_in()Si hay una cuenta vinculada
client.me()Tu JID de número, con dispositivo
client.lid()Tu LID
client.latency()La latencia de red medida en segundo plano

Empieza

Conceptos

Las pocas ideas que necesitas para entender todo lo demás.

Dispositivo vinculado#

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.

JID: la dirección de todo#

Cada cuenta, grupo o canal tiene un JID con la forma usuario:dispositivo@servidor.

EjemploQué esCómo crearlo en Rust
5215512345678@s.whatsapp.netUna cuenta, por su númeroJid::phone("5215512345678")
5215512345678:3@s.whatsapp.netEl dispositivo 3 de esa cuentajid.with_device(3)
100000000000001@lidUna cuenta, por su LIDJid::lid("100000000000001")
120363000000000001@g.usUn grupoJid::group("120363000000000001")
120363000000000001@newsletterUn canalJid::new("120363000000000001", "newsletter")
status@broadcastLos estadosJid::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.

LID: cuando WhatsApp oculta 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.

Chat y remitente#

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.

Identificador y clave de un mensaje#

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.

Mensajes envueltos#

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.

Eventos#

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.

Colas por chat#

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.

Sesiones, cuentas y almacenamiento#

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.

Texto#

use soblendwa::Jid;

let chat = Jid::phone("5215512345678");
let id = client.send_text(&chat, "Hola desde SoblendWA").await?;

Si el texto trae un enlace, la vista previa se genera sola.

Responder#

reply cita el mensaje original y responde en el mismo chat.

client.reply(&msg, "Respondiendo a tu mensaje").await?;

También puedes citar en cualquier otro mensaje: Message::text("hola").reply_to(&msg) en Rust, o { quoted: message } en las opciones de JavaScript.

Reaccionar#

client.react(&msg, "🔥").await?;

Para quitar tu reacción, envía un emoji vacío.

Menciones#

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?;

Editar y eliminar#

let id = client.send_text(&chat, "versión 1").await?;
client.edit_message(&chat, &id, "versión 2").await?;
client.revoke_message(&chat, msg.key()).await?;

Fijar y desfijar#

client.pin_message(&chat, msg.key(), 7 * 24 * 60 * 60).await?;
client.unpin_message(&chat, msg.key()).await?;

Reenviar#

.forwarded() marca el mensaje como reenviado.

client.send_message(&chat, Message::text("reenviado").forwarded()).await?;

Ubicación#

client.send_message(&chat, Message::location(19.4326, -99.1332)).await?;

Contactos#

let tarjeta = Message::vcard("Ana", "+52 1 55 1111 1111");
client.send_message(&chat, Message::contacts("Ana", [("Ana", tarjeta)])).await?;

Invitar a un grupo#

Envía una tarjeta de invitación. SoblendWA pide el código del grupo por ti, así que debes ser administrador.

client.send_group_invite(&chat, &grupo, Some("Únete al equipo")).await?;

Compartir o pedir un número#

client.send_message(&chat, Message::share_phone_number()).await?;
client.send_message(&chat, Message::request_phone_number()).await?;

Limitar el reenvío#

Activa o desactiva el límite de reenvío de una conversación.

client.send_message(&chat, Message::limit_sharing(true)).await?;

Estados#

Publica una historia para una lista de contactos.

client.send_status(Message::text("Mi primer estado"), &[Jid::phone("5215511111111")]).await?;

Etiqueta de miembro#

Pone tu etiqueta dentro de un grupo, de hasta 30 caracteres. Experimental

client.set_member_label(&grupo, "Soporte").await?;

Cualquier otro mensaje#

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?;

Opciones de envío#

send_message_with da control total sobre un envío.

use soblendwa::{PreviewChoice, SendOptions};

let options = SendOptions { id: Some("3EB0MIID".into()), link_preview: PreviewChoice::Off, ..Default::default() };
client.send_message_with(&chat, Message::text("hola"), options).await?;
CampoPara qué
idUsa este id en lugar de generar uno
editMarca de edición de bajo nivel. edit_message ya la usa por ti
status_recipientsDestinatarios de un estado
link_previewAuto, On, Off o Custom. Ver Vista previa

Qué puede fallar#

ErrorQué significaQué hacer
Error::RateLimitedPasaste el límite de envíosEspera retry_after
Error::NotLoggedInLa cuenta aún no está vinculadaVincúlala primero
Error::AccountBlockedWhatsApp bloqueó la cuentaRevisa Confiabilidad
Error::InvalidJidEl destino no es válidoRevisa el chat

Si no hay conexión, el mensaje se guarda en el outbox y sale al reconectar. La confirmación de WhatsApp llega después en Event::ServerAck.

Mensajes

Recibir mensajes

Qué trae cada mensaje, cómo leerlo y cómo evitar responder a lo viejo.

El bucle de eventos#

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());
    }
}

También llegan tus propios mensajes, enviados desde el teléfono o desde el bot: revisa siempre from_me.

Datos del mensaje#

msg.info trae el contexto y msg.message el contenido, ya desenvuelto.

RustJSONQué es
info.idinfo.idIdentificador del mensaje
info.chatinfo.chatChat donde llegó
info.senderinfo.senderQuién lo envió, tal como llegó
info.sender_altinfo.senderAltLa otra identidad, si WhatsApp la envió
info.sender_phoneinfo.senderPhoneSu JID de número, si se conoce
info.sender_lidinfo.senderLidSu LID, si se conoce
info.from_meinfo.fromMeSi lo enviaste tú
info.is_groupinfo.isGroupSi viene de un grupo
info.push_nameinfo.pushNameEl nombre que eligió esa persona
info.timestampinfo.timestampHora de envío, en segundos Unix
info.categoryinfo.categoryCategoría de WhatsApp, como peer
info.offlineinfo.offlineSi llegó mientras estabas desconectado
info.editinfo.editMarca de edición, si la hay
info.server_idinfo.serverIdId de servidor, en canales
info.elapsed()info.elapsedMicrosTiempo dentro del bot desde que llegó del socket

Leer el contenido#

RustJSONQué devuelve
msg.text()textEl texto o el caption, aunque venga envuelto
msg.message.media()mediaLa multimedia. En JSON: tipo, mimetype, tamaño, caption, duración y dimensiones
msg.message.media_type()mediaTypeEl tipo de multimedia
msg.message.stanza_type()kindEl tipo de mensaje
msg.quoted()quotedLo citado. En JSON: id, remitente, chat, texto y multimedia
msg.mentions()mentionsLos JID mencionados
msg.is_reaction()reactionSi es una reacción. En JSON: targetId y emoji
msg.message.is_view_once()viewOnceSi es de una sola vista
msg.message.interactive_reply()replyLa opción elegida en un botón o lista
msg.message.link_thumbnail()linkPreviewDatos de la vista previa recibida
msg.rawprotoEl protobuf original. En JSON, en base64
—ageSecsAntigüedad en segundos

Evitar comandos viejos#

Los mensajes que llegaron con el bot apagado traen info.offline = true. is_stale combina eso con la antigüedad.

use std::time::Duration;

if msg.info.is_stale(Duration::from_secs(120)) {
    return Ok(());
}

Cuando terminan de llegar los mensajes pendientes se emite Event::OfflineSyncComplete { count }.

Duplicados#

SoblendWA descarta por id los duplicados que llegan, y los cuenta en stats().duplicates, así que no tienes que hacerlo tú.

Mensajes que no se pudieron descifrar#

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.

Marcar como leído#

client.read_message(&msg).await?;
client.mark_read(&chat, None, &[msg.info.id.clone()]).await?;
client.mark_played(&msg).await?;

mark_played es para notas de voz y videos circulares. Para otros tipos de recibo está send_receipt.

Recibos de los demás#

Event::Receipt avisa de lo que pasa con lo que enviaste: Delivered, Read, Played, Retry y otros tipos, que están en Eventos.

Responder por id en otros lenguajes#

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.

Enviar#

client.send_image(&chat, &std::fs::read("foto.jpg")?, Some("mi foto")).await?;
client.send_video(&chat, &std::fs::read("clip.mp4")?, Some("mi video")).await?;
client.send_audio(&chat, &std::fs::read("nota.ogg")?, true).await?;
client.send_document(&chat, &std::fs::read("reporte.pdf")?, "reporte.pdf", "application/pdf").await?;
client.send_sticker(&chat, &std::fs::read("sticker.webp")?).await?;

Las dimensiones y las miniaturas se calculan solas. Para notas de voz usa audio Opus en un contenedor OGG, que es lo que espera WhatsApp.

Opciones#

Rust (MediaOptions)JavaScriptPythonPara qué
captioncaptioncaptionTexto que acompaña
mimetypemimetypemimetypeTipo MIME, si no se deduce solo
file_namefileNamefile_nameNombre del documento
voice_notevoiceNotevoice_noteEnviar el audio como nota de voz
gifgifgifReproducir el video como GIF
view_onceviewOnceview_onceDe una sola vista
secondssecondssecondsDuración
width, height——Dimensiones, si no quieres las calculadas
thumbnail——Miniatura JPEG propia
—quotedquotedCitar un mensaje

Preparar antes de enviar#

prepare_media sube el archivo y te devuelve un Message al que puedes añadir lo que quieras antes de enviarlo.

use soblendwa::{MediaKind, MediaOptions};

let options = MediaOptions { caption: Some("una sola vez".into()), view_once: true, ..Default::default() };
let mensaje = client.prepare_media(&std::fs::read("foto.jpg")?, MediaKind::Image, options).await?;
client.send_message(&chat, mensaje.reply_to(&msg)).await?;

GIF y video circular#

let gif = MediaOptions { gif: true, ..Default::default() };
client.send_media(&chat, &std::fs::read("anim.mp4")?, MediaKind::Video, gif).await?;

let video = client.prepare_media(&std::fs::read("clip.mp4")?, MediaKind::Video, MediaOptions::default()).await?;
client.send_message(&chat, Message::video_note(video)).await?;

Álbumes#

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.

Descargar#

if msg.message.media().is_some() {
    let bytes = client.download_message(&msg.message).await?;
    std::fs::write(format!("{}.bin", msg.info.id), bytes)?;
}

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

Archivos grandes#

Con estos métodos la memoria no depende del tamaño del archivo.

client.send_media_file(&chat, "pelicula.mp4", MediaKind::Video, MediaOptions::default()).await?;
client.send_media_file(&chat, "reporte.pdf", MediaKind::Document, MediaOptions::default()).await?;
let bytes = client.download_event_to_file(&msg, "descargas/video.mp4").await?;

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étodoCuándo usarlo
upload_fileSubir un archivo del disco sin enviarlo todavía
upload_readerFuentes que solo se leen una vez, como un socket. Cifra hacia un archivo temporal y lo borra al terminar
prepare_media_fileComo prepare_media, desde una ruta
download_to_writerDescargar hacia cualquier escritor

Reutilización automática#

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.

Herramientas de imagen#

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.

Por mensaje#

use soblendwa::{LinkPreview, PreviewChoice, SendOptions};

client.send_text(&chat, "Mira esto: https://www.rust-lang.org").await?;

let sin_vista = SendOptions { link_preview: PreviewChoice::Off, ..Default::default() };
client.send_message_with(&chat, Message::text("https://www.rust-lang.org"), sin_vista).await?;

let propia = LinkPreview { title: Some("Mi título".into()), description: Some("Escrito a mano".into()), ..Default::default() };
let opciones = SendOptions { link_preview: PreviewChoice::Custom(Box::new(propia)), ..Default::default() };
client.send_message_with(&chat, Message::text("https://www.rust-lang.org"), opciones).await?;
PreviewChoiceJavaScript y PythonQué hace
Autosin opciónGenera la vista previa si hay un enlace. Es lo normal
Onpreview: trueLa pide de forma explícita
Offpreview: falseSin vista previa
Custom(..)preview: { … }Usa tus datos: url, title, description, thumbnail, image, width, height y video

Para todo el cliente#

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?;
RustJavaScriptPor defectoPara qué
enabledenabledtrueActiva las vistas previas
timeouttimeoutMs3 sTiempo máximo por petición
upload_hq_thumbnailuploadHqThumbnailtrueSube la miniatura nítida
allow_private_hostsallowPrivateHostsfalsePermite consultar hosts privados
max_html_bytesmaxHtmlBytes512 KiBMáximo de HTML leído
max_image_bytesmaxImageBytes4 MiBMáximo de imagen descargada
user_agentuserAgent—Agente de usuario de la consulta
cache_ttlcacheTtlSecs1 horaCuánto se recuerda un enlace
fetcher—NingunoTu propia fuente de datos, en Rust

Cómo funciona#

  • 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.

Tu propia fuente de datos#

Implementa LinkPreviewFetcher para traer los datos de otro servicio. Si devuelves None, el enlace se envía sin tus datos.

use std::sync::Arc;
use async_trait::async_trait;
use soblendwa::{LinkPreview, LinkPreviewConfig, LinkPreviewFetcher};

struct MiFuente;

#[async_trait]
impl LinkPreviewFetcher for MiFuente {
    async fn fetch(&self, url: &str) -> soblendwa::Result<Option<LinkPreview>> {
        Ok(Some(LinkPreview { url: url.to_owned(), title: Some("Desde mi API".into()), ..Default::default() }))
    }
}

let config = LinkPreviewConfig { fetcher: Some(Arc::new(MiFuente)), ..Default::default() };

Otras funciones#

MétodoQué hace
client.link_preview(url)Obtiene los datos de un enlace sin enviar nada
client.with_link_preview(message)Devuelve el mensaje con su vista previa añadida
client.send_text_with_preview(chat, text)Envía el texto pidiendo la vista previa
client.set_link_previews_disabled(true)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.

Crear una encuesta#

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?;

Message::announcement_poll es para grupos de anuncios de comunidades.

Votar#

client.vote_poll(&chat, &encuesta, &["Tacos"]).await?;

Leer los votos#

if let Event::PollVote(voto) = event {
    println!("{} votó {:?} en {}", voto.voter, voto.selected, voto.poll_id);
}

Las opciones llegan por nombre. Si activas el store de chats, los votos también se guardan en poll_votes.

Eventos de calendario#

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.

Leer las respuestas#

Event::EventResponse trae chat, event_id, responder, kind, extra_guests y timestamp.

kind en RustEn JSONQué significa
GoinggoingVa
NotGoingnotGoingNo va
MaybemaybeTal vez
UnknownunknownRespuesta no reconocida

Mensajes

Botones y listas

Los mensajes interactivos con el formato que WhatsApp sí muestra.

Menú interactivo#

use soblendwa::interactive::{Interactive, Section};

let menu = Interactive::new("¿Qué quieres hacer?")
    .title("Soblend")
    .footer("SoblendWA")
    .quick_reply("ping", "Medir latencia")
    .url("Documentación", "https://github.com/Soblend-Development-Studio/SoblendWA")
    .copy("Copiar cupón", "SOBLEND-2026")
    .call("Llamar", "+5215512345678")
    .select("Ver comandos", vec![Section::new("Comandos").described_row("menu", "Menú", "Todos los comandos")]);
client.send_message(&chat, menu.build()).await?;
BotónRustJSONQué hace
Respuesta rápida.quick_reply(id, texto){ type: 'reply', id, text }Envía una respuesta con ese id
Abrir enlace.url(texto, url){ type: 'url', text, url }Abre la página
Copiar.copy(texto, código){ type: 'copy', text, code }Copia un código
Llamar.call(texto, teléfono){ type: 'call', text, phone }Inicia una llamada
Menú.select(título, secciones){ type: 'select', title, sections }Abre una lista de opciones

La cabecera puede llevar texto (.title, .subtitle) o multimedia ya preparada (.image, .video, .document).

Botones simples#

client.send_message(&chat, Message::buttons("Elige", Some("pie"), [("si", "Sí"), ("no", "No")])).await?;

Listas#

use soblendwa::interactive::Section;

client.send_message(&chat, Message::list("Menú", "Elige un plato", "Ver menú", None, &[
    Section::new("Tacos").row("pastor", "Al pastor").row("suadero", "Suadero"),
])).await?;

Leer la opción elegida#

if let Some(reply) = msg.message.interactive_reply() {
    println!("eligió {} ({:?})", reply.id, reply.source);
}

reply trae id, text, source (buttons, list, template o nativeFlow), flow y params.

Plantillas y respuestas#

MétodoQué hace
Message::template(texto, pie, botones)Mensaje de plantilla con TemplateButton
Message::button_reply(id, texto)Responde a un botón
Message::template_reply(id, texto, índice)Responde a una plantilla
Message::list_reply(título, id_fila)Responde a una lista

Cuentas y chats

Grupos

Crear grupos, gestionar participantes, cambiar ajustes y usar invitaciones.

Consultar#

let info = client.group_info(&grupo).await?;
let actualizado = client.group_metadata(&grupo).await?;
let todos = client.joined_groups().await?;

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.

Revisar si alguien es admin#

is_admin acepta el número o el LID de la persona, así que funciona en grupos con LID.

let grupo_info = client.group_info(msg.chat()).await?;
if grupo_info.is_admin(msg.sender()) {
    client.reply(&msg, "Eres administrador").await?;
}
Método de GroupInfoQué devuelve
is_admin(jid)Si esa persona es administradora
participant(jid)El participante, buscando por número o por LID
admins()Los administradores
is_lid_addressed()Si el grupo usa LID

Crear y gestionar#

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ó.

Ajustes#

MétodoQué cambia
set_group_announce(grupo, true)Solo los administradores pueden escribir
set_group_locked(grupo, true)Solo los administradores pueden cambiar los datos del grupo
set_group_ephemeral(grupo, segundos)Mensajes temporales. 0 los desactiva
set_group_join_approval(grupo, true)Un administrador aprueba cada solicitud para entrar
set_group_member_add_mode(grupo, true)Todos los miembros pueden añadir gente. Con false, solo los administradores

Invitaciones#

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.

Solicitudes para entrar#

let solicitudes = client.group_join_requests(&grupo).await?;
let personas: Vec<Jid> = solicitudes.iter().map(|s| s.jid.clone()).collect();
client.respond_join_requests(&grupo, &personas, true).await?;

Cambios en vivo#

Event::GroupUpdate avisa de lo que cambia: group, author, action, participants y timestamp.

GroupActionEn JSONQué significa
CreatedcreatedSe creó el grupo
Add, Remove, Leaveadd, remove, leaveEntró, sacaron o se fue alguien
Promote, Demotepromote, demoteNombraron o quitaron un administrador
Subject(texto)subjectCambió el nombre
Description(texto)descriptionCambió la descripción
Announce(bool)announceSolo administradores escriben
Locked(bool)lockedSolo administradores editan los datos
Ephemeral(segundos)ephemeralCambiaron los mensajes temporales
Other(nombre)otherOtro cambio

Cuando alguien sale de un grupo, SoblendWA rota la sender key para que esa persona no pueda leer lo que venga después.

Datos de GroupInfo#

CampoQué es
jid, subject, descriptionIdentificador, nombre y descripción
owner, creationCreador y fecha de creación
subject_owner, subject_time, description_idQuién puso el nombre, cuándo, y el id de la descripción
announce, locked, join_approval, members_can_addAjustes
ephemeralSegundos de los mensajes temporales
addressing_modeCómo se direcciona, por número o por LID
linked_parent, is_community, is_community_announceRelación con comunidades
participantsLista 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.

Cuentas y chats

Canales

Crear, seguir y publicar en canales de WhatsApp.

Consultar y seguir#

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?;

Leer y reaccionar#

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.

Crear y administrar Experimental#

let canal = client.newsletter_create("Soblend News", Some("Novedades del estudio")).await?;
client.newsletter_update(&canal.jid, Some("Soblend"), None, None).await?;
let suscriptores = client.newsletter_subscribers(&canal.jid).await?;
let admins = client.newsletter_admin_count(&canal.jid).await?;
client.newsletter_demote(&canal.jid, &Jid::phone("5215511111111")).await?;
client.newsletter_change_owner(&canal.jid, &Jid::phone("5215522222222")).await?;
client.newsletter_delete(&canal.jid).await?;

Para publicar, envía un mensaje normal a su JID con send_message.

Datos#

NewsletterInfoQué es
jid, name, description, inviteIdentificación
subscribers, verified, mutedEstado
picture_path, role, createdFoto, tu rol y fecha
rawLa respuesta completa en JSON
NewsletterMessageQué es
server_id, id, timestampIdentificación
views, reactionsVistas y reacciones
messageEl contenido

Cuentas y chats

Chats, contactos y etiquetas

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.

Organizar chats#

use std::time::Duration;

client.archive_chat(&chat, true).await?;
client.pin_chat(&chat, true).await?;
client.mute_chat(&chat, Some(Duration::from_secs(8 * 60 * 60))).await?;
client.mark_chat_read(&chat, true).await?;
client.clear_chat(&chat).await?;
client.delete_chat(&chat).await?;

Las acciones de modifyChat son archive, unarchive, pin, unpin, mute, unmute, read, unread, delete y clear. Si no indicas segundos, mute dura 8 horas.

Mensajes destacados y contactos#

client.star_message(&chat, &msg.info.id, false, true).await?;
client.save_contact(&Jid::phone("5215511111111"), "Ana Soblend").await?;
client.set_push_name("Mi Bot").await?;

Etiquetas Experimental#

client.edit_label("1", "Clientes VIP", 3).await?;
client.label_chat(&chat, "1", true).await?;
client.label_message(&chat, &msg.info.id, "1", true).await?;
client.delete_label("1").await?;

Respuestas rápidas Experimental#

let id = client.save_quick_reply(None, "hola", "¡Hola! ¿En qué te ayudo?").await?;
client.delete_quick_reply(&id).await?;

Cualquier otro cambio#

chat_modify acepta todas las modificaciones, incluidas las que no tienen atajo.

use soblendwa::ChatModification;

client.chat_modify(&chat, ChatModification::RemoveContact).await?;
client.chat_modify(&chat, ChatModification::DeleteForMe {
    message_id: msg.info.id.clone(),
    from_me: msg.info.from_me,
    timestamp: msg.info.timestamp,
}).await?;

ChatModification puede ser Archive, Pin, Mute, MarkRead, Delete, Clear, Star, DeleteForMe, SaveContact, RemoveContact, Label o MessageLabel.

Cambios desde el teléfono#

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}");
}
ChatUpdateEn JSONCampos
Mutedmutedchat, until
Archivedarchivedchat, archived
Pinnedpinnedchat, pinned
MarkedReadmarkedReadchat, read
Lockedlockedchat, locked
Deleted, Cleareddeleted, clearedchat
Starredstarredchat, message_id, from_me, starred
DeletedForMedeletedForMechat, message_id, from_me
Contactcontactjid, full_name, first_name, lid, phone
ContactRemovedcontactRemovedjid
PushNamepushNamename
Labellabelid, name, color, deleted
ChatLabelchatLabellabel, chat, labeled
MessageLabelmessageLabellabel, chat, message_id, labeled
Otherotherindex

En JSON, los campos usan camelCase, como messageId o fullName.

Sincronizar a mano#

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.

Activarlo#

use soblendwa::ChatStore;

let chats = ChatStore::open("session/chats.json").await?;
let (client, events) = Client::builder().store(store).chat_store(chats.clone()).build().await?;

El servidor lo activa por defecto, en una carpeta por cuenta. Se desactiva con --no-chat-store.

Consultar#

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?;

Qué guarda#

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.

TipoCampos
Chatjid, name, unread, last_message_at, archived, pinned, muted_until, locked, labels
Contactjid, name, first_name, push_name, lid, phone, y display_name()
StoredMessageid, chat, sender, from_me, timestamp, push_name, message, status, starred, edited, revoked, reactions, poll_votes

status puede ser Error, Pending, Sent, Delivered, Read o Played. Los métodos completos están en la referencia de ChatStore.

Historial#

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.

Foto de perfil#

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).

Información y nombre#

client.set_about("Hecho con SoblendWA").await?;
let infos = client.fetch_about(&[chat.clone()]).await?;
client.set_push_name("Mi Bot").await?;

Bloquear#

client.block(&chat).await?;
client.unblock(&chat).await?;
let bloqueados = client.blocklist().await?;

Privacidad#

use soblendwa::{PrivacySetting, PrivacyValue};

client.set_privacy(PrivacySetting::LastSeen, PrivacyValue::Contacts).await?;
let ajustes = client.privacy_settings().await?;
PrivacySettingQué controla
LastSeenÚltima vez
OnlineEn línea
ProfilePictureFoto de perfil
AboutInformación
ReadReceiptsConfirmaciones de lectura
GroupsAddQuién te añade a grupos
CallsQuién te llama
MessagesQuié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

Mensajes temporales#

client.set_disappearing(&chat, 7 * 24 * 60 * 60).await?;
client.set_default_disappearing(0).await?;
let modos = client.disappearing_modes(&[chat.clone()]).await?;

Comprobar números#

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?;

Cada resultado trae query, jid, exists y lid. phone_for_lid solo conoce los pares que WhatsApp ya compartió contigo.

Límites de la cuenta#

let limite = client.reachout_timelock().await?;
let cupo = client.new_chat_message_cap().await?;
let bots = client.bot_list().await?;
MétodoQué devuelve
reachout_timelockSi WhatsApp limitó el contacto con gente nueva: active, ends_at y enforcement
new_chat_message_capCuántos chats nuevos puedes iniciar, como JSON
bot_listLos bots de Meta AI disponibles: jid y persona_id
business_profile(jid)El perfil de empresa de otra cuenta

Cuentas y chats

Presencia y llamadas

En línea, escribiendo, grabando, y lo que puedes hacer con las llamadas.

En línea#

client.set_presence(true).await?;
client.subscribe_presence(&chat).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.

Escribiendo y grabando#

use soblendwa::ChatPresence;

client.send_chat_presence(&chat, ChatPresence::Composing).await?;
client.send_chat_presence(&chat, ChatPresence::Recording).await?;
client.send_chat_presence(&chat, ChatPresence::Paused).await?;

Lo que hacen los demás llega como Event::ChatState { chat, sender, state }.

Llamadas#

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

Catálogo y productos#

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.

Enviar un producto#

client.send_product(&chat, &mi_numero, &producto, &std::fs::read("taza.jpg")?, Some("¡Nuevo!")).await?;

Colecciones y pedidos#

let colecciones = client.catalog_collections(None, 10).await?;
let pedido = client.order_details("id-del-pedido", "token-del-pedido").await?;

Perfil de empresa#

use soblendwa::business::{BusinessHours, BusinessProfileUpdate, OpeningHours, Weekday};

client.update_business_profile(&BusinessProfileUpdate {
    email: Some("ventas@soblend.dev".into()),
    hours: Some(BusinessHours {
        timezone: "America/Mexico_City".into(),
        days: vec![(Weekday::Mon, OpeningHours::Between { open_minute: 9 * 60, close_minute: 18 * 60 })],
    }),
    ..Default::default()
}).await?;

let portada = client.set_business_cover_photo(std::fs::read("portada.jpg")?).await?;
client.remove_business_cover_photo(&portada).await?;
let perfil = client.business_profile(&chat).await?;

OpeningHours puede ser AllDay, ByAppointment o Between { open_minute, close_minute }, con los minutos contados desde la medianoche.

Datos#

TipoCampos
Productid, name, retailer_id, url, description, price, currency, hidden, images, review_status, can_appeal
ProductDraftname, description, retailer_id, price, currency, images, hidden, origin_country
Catalogproducts, next_cursor
ProductCollectionid, name, products, status, can_appeal
Orderitems, total, currency
OrderItemid, name, image_url, price, currency, quantity
BusinessProfilejid, description, address, email, website, category
BusinessProfileUpdatedescription, address, email, websites, hours

Operación

Confiabilidad

Pensado para bots que corren meses sin que nadie los mire, con varias cuentas en el mismo proceso.

Si WhatsApp bloquea la cuenta#

Se detiene y te dice por qué, sin borrar la sesión. Si desbloqueas la cuenta en el teléfono, vuelves a conectar sin escanear otro QR.

CódigoSignificado
402Baneo temporal
403Cuenta bloqueada
406Baneo
if let Event::AccountBlocked { code, reason, expires } = event {
    eprintln!("Bloqueada ({code}): {reason}. Hasta: {expires:?}");
}

Reconexión#

La espera crece y tiene un margen aleatorio, para que cien sesiones no se reconecten en el mismo milisegundo.

Fallo seguidoEspera
PrimeroDe 0 a 0,75 s
Segundo1 s más un margen aleatorio
Tercero2 s más un margen aleatorio
Siguientes4 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.

Límite de envíos#

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 SendLimitPor defectoQué limita
per_minute240Mensajes por minuto, en total
burst60Ráfaga máxima, en total
per_chat_per_minute40Mensajes por minuto, en cada chat
per_chat_burst15Rá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.

Reinicios sin pérdidas#

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.
  • stats().outbox_resent cuenta los reenvíos.
  • .outbox(false) lo desactiva.

Reintentos después de reiniciar#

Lo enviado se conserva hasta dos días, así un contacto que no pudo descifrar un mensaje puede pedirlo de nuevo aunque el bot se haya reiniciado.

Un fallo no tumba todo#

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.

Recuperación de mensajes#

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

Tiempos por fase#

Cada Event::ServerAck trae timings, para saber dónde se fue el tiempo de un envío.

CampoEn JSONQué mide
queuedqueuedMicrosEspera en la cola de su chat
uploaduploadMicrosSubida de multimedia, si la hubo
prepareprepareMicrosDispositivos y sesiones
encryptencryptMicrosCifrado
serverserverMicrosRespuesta de WhatsApp
total()totalMicrosLa suma

Un ServerAck con error trae el código de rechazo de WhatsApp. Uno sin timings suele ser una petición interna al teléfono.

Métricas#

let stats = client.stats();
println!("{} recibidos, {} enviados", stats.received, stats.sent);
RustJSONQué cuenta
connectedconnectedSi está conectado ahora
uptimeuptimeSecsTiempo desde que arrancó
received, sentreceived, sentMensajes recibidos y enviados
undecryptableundecryptableLos que no se pudieron descifrar
recoveredrecoveredLos que se recuperaron con el reintento
awaiting_recoveryawaitingRecoveryLos que siguen esperando
duplicatesduplicatesDuplicados descartados
reconnectsreconnectsReconexiones
rate_limitedrateLimitedEnvíos frenados por el límite
media_uploaded, media_reusedmediaUploaded, mediaReusedArchivos subidos y reutilizados
outbox_resentoutboxResentReenvíos del outbox
panicspanicsPánicos contenidos
cached_devices, cached_groupscachedDevices, cachedGroupsTamaño de las cachés
pending_queriespendingQueriesConsultas esperando respuesta
open_lanesopenLanesColas de chat abiertas
latencylatencyMicrosLatencia de red

Latencia#

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 completo contra zapo#

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)SoblendWAzapo-js 1.11
Ida y vuelta, p50 / p95 / p994,2 / 10,1 / 16,3 ms10,5 / 18,4 / 23,3 ms
Mensajes por segundo en ráfaga474228
CPU del bot por mensaje en ráfaga0,7 ms4,5 ms
Memoria tras la prueba (pico)12 MB (24 MB)130 MB (131 MB)
Conectar y vincular100 ms1.565 ms

Las piezas, una por una#

SoblendWA (Rust)whatsmeow (Go)Baileys 7 (JavaScript)
Codificar + decodificar un stanza de 21 nodos (3.375 bytes)63.242 stanzas/s12.810 (4,9× menos)5.320 (11,9× menos)
Cifrar + descifrar un mensaje 1:1 (Signal)135.504 msg/s12.223 (11× menos)3.720 (36× menos)
Cifrar + descifrar un mensaje de grupo (Sender Keys)9.117 msg/s3.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.

Puedes repetir lo nuestro:

Terminal
cargo test --release -- --ignored throughput

Por qué es rápido#

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.

Elegir un store#

StoreFeaturePara quéEstado
FileStoreincluidoUn bot en una máquina. Log de solo-escritura con compactaciónProbado
MemoryStoreincluidoPruebas. Se pierde todo al salirProbado
SqliteStoresqliteUn archivo, varias cuentas, sin servidor. SQLite va incluido en el binarioProbado
PostgresStorepostgresMuchos bots o servidores compartiendo una baseExperimental
RedisStoreredisContenedores efímeros con estado en RedisExperimental
MongoStoremongoBots que ya usan MongoDBExperimental
Tu propio store—Cualquier base de datos, en Rust o en JavaScriptProbado

Los marcados como experimentales pasan sus pruebas locales, pero todavía no se han probado contra un servidor real.

Abrirlos#

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.

Cómo funciona por dentro#

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.

Store escrito en JavaScript#

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.

const store = {
  loadCreds: () => db.creds.findOne(),
  saveCreds: (json) => db.creds.save(json),
  get: (kind, key) => db.keys.get(`${kind}:${key}`),
  keys: (kind) => db.keys.list(kind),
  apply: (writes) => db.keys.bulk(writes),
}
const bot = await SoblendWA.create({ store })
FunciónRecibeDevuelve
loadCreds()—El texto JSON guardado, o null
saveCreds(json)Un texto JSON—
get(kind, key)Tipo y claveUn Buffer, un Uint8Array o null
keys(kind)TipoLista 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.

Store escrito en Rust#

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.

use async_trait::async_trait;
use soblendwa::store::{Creds, KeyKind, KeyWrite};
use soblendwa::{Backend, Result};

struct MiBaseDeDatos;

#[async_trait]
impl Backend for MiBaseDeDatos {
    async fn load_creds(&self) -> Result<Option<Creds>> {
        Ok(None)
    }

    async fn save_creds(&self, _creds: &Creds) -> Result<()> {
        Ok(())
    }

    async fn fetch(&self, _kind: KeyKind, _key: &str) -> Result<Option<Vec<u8>>> {
        Ok(None)
    }

    async fn apply(&self, _writes: &[KeyWrite]) -> Result<()> {
        Ok(())
    }
}

Si prefieres controlarlo todo, implementa directamente el trait Store, con load_creds, save_creds, get, set, remove, clear, y opcionalmente flush y keys.

Qué se guarda#

KeyKind indica el tipo de cada dato.

KeyKindQué guarda
SessionSesiones Signal con cada dispositivo
PreKeyTus pre-keys
SenderKey, SenderKeyMemoryClaves de grupo y a quién se le enviaron
LidMappingPares número ↔ LID
DeviceListDispositivos de cada cuenta
GroupMetadataDatos de los grupos
PollSecretSecretos para descifrar votos
AppStateKey, AppStateVersionClaves y versiones del app state
TcTokenTokens de privacidad
MediaCacheMultimedia reutilizable
OutboxMensajes sin confirmar
RecentSendEnvíos recientes, para atender reintentos

Operación

Configuración

Todas las opciones del cliente, con sus valores por defecto.

use std::time::Duration;
use soblendwa::{Client, Platform, SendLimit};

let (client, events) = Client::builder()
    .store(store)
    .device_name("SoblendWA")
    .platform(Platform::Desktop)
    .push_name("Mi Bot")
    .online_on_connect(false)
    .auto_reconnect(true)
    .query_timeout(Duration::from_secs(30))
    .send_limit(Some(SendLimit { per_minute: 120, burst: 30, per_chat_per_minute: 20, per_chat_burst: 8 }))
    .outbox(true)
    .media_cache(Some(Duration::from_secs(7 * 24 * 60 * 60)))
    .workers(16)
    .build()
    .await?;
MétodoPor defectoPara qué
store(store)MemoriaDónde se guarda la sesión
chat_store(chats)NingunoActiva el store de chats
device_name(texto)SoblendWANombre en Dispositivos vinculados
platform(plataforma)ChromeChrome, Firefox, Safari, Edge, Opera o Desktop
push_name(texto)NingunoTu nombre visible
country_code(texto)USPaís que se le informa a WhatsApp
version([a, b, c])AutomáticaFija la versión de WhatsApp Web, en lugar de la más reciente
online_on_connect(bool)trueAparecer en línea al conectar
auto_reconnect(bool)trueReconectar solo
query_timeout(duración)60 sTiempo máximo para una consulta
send_limit(límite)240/minLímite de envíos
outbox(bool)trueOutbox persistente
media_cache(duración)7 díasReutilización de multimedia
link_previews(config)ActivadasVista previa de enlaces
proxy(proxy)NingunoProxy
workers(número)Núcleos × 2, entre 4 y 64Mensajes procesados en paralelo
config(config)—Reemplaza toda la configuración de una vez
ws_url(url)WhatsApp realConectar a otro servidor, como uno falso
trust_root(clave)La de WhatsAppRaíz de los certificados de ese servidor
media_endpoint(origen)AutomáticoServidor de multimedia alternativo

Config tiene además connect_timeout (20 s) y keepalive_interval (15 s), que se cambian a través de .config(...).

En Node.js y Python#

Las opciones se pasan a SoblendWA.create. En Python usan snake_case, y se convierten solas.

JavaScriptPythonRust
storestorestore
chatStore: { path, limit }chat_store={...}chat_store
pushNamepush_namepush_name
deviceNamedevice_namedevice_name
platformplatformplatform
countryCodecountry_codecountry_code
onlineOnConnectonline_on_connectonline_on_connect
autoReconnectauto_reconnectauto_reconnect
proxyproxyproxy
linkPreviewslink_previewslink_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.

Registros#

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.

Terminal
RUST_LOG=soblendwa=debug,soblendwa_server=info soblendwa-server

Operación

Proxy

Pasa toda la conexión por un proxy HTTP o SOCKS5.

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?;
EsquemaQué es
http://Proxy HTTP con CONNECT
socks5://SOCKS5
socks5h://SOCKS5 resolviendo los nombres en el proxy

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.

Desde zapo con SQLite#

Terminal
cargo run --release --example import_zapo --features sqlite -- ./auth/state.sqlite bot ./session

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.

Volver a zapo#

Si algo no te convence, escribe el estado actual de SoblendWA de vuelta en la base de zapo y sigue con tu bot de antes, con los ratchets al día.

Terminal
cargo run --release --example import_zapo --features sqlite -- --rollback ./session ./auth/state.sqlite bot

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.

Desde tu código#

use soblendwa::store::{export_zapo_sqlite, import_zapo_sqlite, zapo_sessions};

let sesiones = zapo_sessions("auth/state.sqlite")?;
let destino = FileStore::open("session").await?;
let reporte = import_zapo_sqlite("auth/state.sqlite", "bot", destino.as_ref()).await?;
println!("{} sesiones, {} pre-keys", reporte.sessions, reporte.pre_keys);
export_zapo_sqlite(destino.as_ref(), "auth/state.sqlite", "bot").await?;

Desde zapo con MongoDB Experimental#

Terminal
cargo run --release --example import_zapo --features "sqlite mongo" -- --mongo mongodb://localhost:27017 mi_base bot ./session
cargo run --release --example import_zapo --features "sqlite mongo" -- --rollback-mongo ./session mongodb://localhost:27017 mi_base bot

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(...).

Desde Baileys#

cargo run --release --example import_baileys -- ./auth_info ./session

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.

Equivalencias con Baileys#

Cada función pública de Baileys 7 tiene su equivalente.

BaileysSoblendWA
useMultiFileAuthStateFileStore, e import_baileys para traer tu sesión
makeInMemoryStoreChatStore
sendMessage({ text }), { image }, { video }…send_text, send_image, send_media_file, prepare_media + send_message
{ ptv: true }Message::video_note
{ contacts }, { location }Message::contacts, Message::vcard, Message::location
{ react }, { delete }, { edit }, { forward }react, revoke_message, edit_message, .forwarded()
{ poll }, { event }, { album }, { pin }Message::poll / announcement_poll, Message::event(..).event_details(..), send_album, pin_message
{ groupInvite }, { product }send_group_invite, send_product
{ buttonReply }, { listReply }Message::button_reply, template_reply, list_reply
{ sharePhoneNumber }, { requestPhoneNumber }, { limitSharing }Message::share_phone_number, request_phone_number, limit_sharing
mentions, mentionAll.mention(..), .mention_all()
readMessages, sendReceipt(s)read_message, mark_read, mark_played, send_receipt
chatModify, star, addOrEditContact, removeContactarchive_chat, pin_chat, mute_chat, mark_chat_read, delete_chat, clear_chat, star_message, save_contact, chat_modify
addLabel, addChatLabel, addMessageLabel (y sus remove)edit_label, delete_label, label_chat, label_message
addOrEditQuickReplysave_quick_reply, delete_quick_reply
update*Privacy, updateDisableLinkPreviewsPrivacyset_privacy, set_link_previews_disabled
updateDefaultDisappearingMode, disappearingMessagesInChat, fetchDisappearingDurationset_default_disappearing, set_disappearing, disappearing_modes
fetchStatus, updateProfileStatus, updateProfileNamefetch_about, set_about, set_push_name
profilePictureUrl, updateProfilePicture, removeProfilePictureprofile_picture_url, set_profile_picture, remove_profile_picture
updateBlockStatus, fetchBlocklist, fetchPrivacySettingsblock, unblock, blocklist, privacy_settings
onWhatsApp, getBusinessProfileon_whatsapp, business_profile
group*group_info, group_metadata, create_group, update_participants, set_group_*, group_invite_code, join_group, accept_group_invite, group_join_requests…
community*create_community, create_community_group, link_group, unlink_group, community_groups, leave_community, y las funciones de grupo
newsletter*, subscribeNewsletterUpdatesnewsletter_*, newsletter_live_updates
getCatalog, getCollections, getOrderDetails, product*, updateBussinesProfile, updateCoverPhotocatalog, catalog_collections, order_details, product_*, update_business_profile, set_business_cover_photo
updateMemberLabel, getBotListV2set_member_label, bot_list
fetchAccountReachoutTimelock, fetchNewChatMessageCapreachout_timelock, new_chat_message_cap
fetchMessageHistory, requestPlaceholderResend, updateMediaMessagerequest_history, request_placeholder_resend, reupload_media
rejectCall, createCallLink, sendPresenceUpdate, presenceSubscribereject_call, create_call_link, send_chat_presence / set_presence, subscribe_presence
requestPairingCode, logout, end, cleanDirtyBits, rotateSignedPreKeyrequest_pair_code, logout, disconnect, clean_dirty_bits, rotate_signed_pre_key
relayMessage, sendNode, querysend_message_with, send_node, query

Operación

Pruebas

Prueba tu bot sin arriesgar ningún número, y vigila que siga funcionando con el tiempo.

WhatsApp falso#

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.

MétodoQué provoca
drop_connections(teléfono)Corta la conexión de esa cuenta
refuse_connections(teléfono, true)Rechaza sus reconexiones
block(teléfono, 403)Bloquea la cuenta con ese código
unblock(teléfono)La desbloquea
hold_acks(teléfono, true)Retiene las confirmaciones
corrupt_next(teléfono, n)Daña los siguientes n mensajes
expire_media()Caduca los archivos subidos
push_node(teléfono, nodo)Envía un nodo cualquiera a esa cuenta
create_group(asunto, creador, miembros)Crea un grupo
media_uploads(), routed(), is_online(teléfono)Consultan contadores y estado
builder(), connect_with(builder)Conectan una cuenta con tu propia configuración

Desde otros lenguajes#

Levanta el servidor contra el WhatsApp falso. Cada sesión recibe un número de prueba, y tu bot se conecta igual que siempre.

Terminal
soblendwa-server --fake --session bot --session ana
import { connect } from 'soblendwa-client'

const bot = await connect('ws://127.0.0.1:8787', { session: 'bot' })
const ana = await connect('ws://127.0.0.1:8787', { session: 'ana' })

bot.on('message', (message) => console.log(message.text))

Nada sale de tu máquina y las sesiones viven en memoria.

Contra un servidor falso de otros#

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:

const bot = await SoblendWA.create({ server: { wsUrl: servidor.url, noiseRootHex: raiz } })

Las pruebas del proyecto#

Terminal
cargo test --features "sqlite mongo testing" --lib
cargo test -p soblendwa-server
cargo test --release -- --ignored throughput
cargo test -p soblendwa-server -- --ignored
ComandoQué prueba
cargo test --features "sqlite mongo testing" --libEl núcleo: 100 pruebas, con flujos completos contra el WhatsApp falso
cargo test -p soblendwa-serverHTTP y WebSocket del servidor
cargo test --release -- --ignored throughputEl rendimiento de las tablas
cargo test -p soblendwa-server -- --ignoredSe conecta a WhatsApp y espera el QR por WebSocket
node bindings/node/test.jsEl módulo de Node.js
python bindings/python/test_bindings.pyEl módulo de Python
node clients/js/test.mjsEl 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.

Bot de pruebas en vivo#

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.

ComandoQué hace
!pingResponde Pong y el tiempo que tardó el bot
!pruebaRevisa de una vez la latencia, los tiempos por fase, la reutilización de multimedia y el outbox
!statsMuestra las métricas
!quienMuestra el número y el LID de quien escribe
!outboxMuestra lo que sigue pendiente

La sesión se guarda en ./session, o en la carpeta que le pases: --example bot -- mi-carpeta.

Pruebas de días#

Terminal
node clients/js/soak.mjs --sessions bot,ana --hours 72 --pid 12345

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.

Canario diario#

Terminal
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.

Crear el cliente#

const { SoblendWA } = require('soblendwa-node')

async function main() {
  const bot = await SoblendWA.create({
    store: { type: 'sqlite', path: 'session/auth.db' },
    pushName: 'Mi Bot',
  })

  bot.on('qr', ({ code }) => console.log('Escanea:', code))
  bot.on('connected', () => console.log('Conectado como', bot.me))
  bot.on('message', async (message) => {
    if (message.info.fromMe) return
    if (message.text === '!ping') await bot.reply(message, `Pong\n${bot.elapsed(message).toFixed(2)} ms`)
  })

  await bot.connect()
}

main()

Con TypeScript es igual, y los tipos vienen en index.d.ts. Las opciones de create están en Configuración.

Eventos#

SoblendWA es un EventEmitter. Cada evento se emite por su type, y también como event, y los errores de los handlers como error.

bot.on('event', (event) => console.log(event.type))
bot.on('error', (error) => console.error(error))

async function recorrer() {
  for await (const event of bot.events()) {
    console.log(event.type)
  }
}

Métodos#

chat acepta un JID completo o solo el número. message acepta el objeto recibido o su id.

connect()

Conecta. Devuelve el propio cliente.

disconnect()

Cierra la conexión y conserva la sesión.

logout()

Desvincula y borra la sesión.

bot.me

Tu JID, o null.

bot.connected

Si está conectado.

ping()

Latencia de red en milisegundos, medida ahora.

stats()

Métricas de la sesión. No es asíncrono.

pendingSends()

Envíos esperando confirmación.

elapsed(message)

Milisegundos que lleva el mensaje dentro del bot. No es asíncrono.

requestPairCode(phone)

Pide un código de vinculación.

sendText(chat, text, { quoted, preview })

Envía texto. Ver Vista previa.

reply(message, text)

Responde citando.

react(message, emoji)

Reacciona.

edit(chat, messageId, text)

Edita un mensaje tuyo.

revoke(chat, messageId)

Elimina para todos.

markPlayed(message)

Marca una nota de voz como reproducida.

sendImage(chat, data, options)

Envía una imagen.

sendVideo(chat, data, options)

Envía un video.

sendAudio(chat, data, options)

Envía audio. Con voiceNote: true, como nota de voz.

sendDocument(chat, data, options)

Envía un documento.

sendSticker(chat, data, options)

Envía un sticker WebP.

sendMedia(chat, kind, data, options)

Envío genérico. kind es image, video, audio, document o sticker.

sendFile(chat, kind, path, options)

Envía un archivo del disco, por bloques y con memoria constante.

download(message)

Descarga y descifra. Devuelve un Buffer.

downloadToFile(message, path)

Descarga a disco por bloques y devuelve los bytes escritos.

sendInteractive(chat, spec)

Mensaje interactivo.

sendButtons(chat, text, buttons, footer)

Botones simples.

sendList(chat, list)

Lista de opciones.

sendPoll(chat, name, options, selectable)

Crea una encuesta.

votePoll(chat, pollId, options)

Vota.

sendContacts(chat, cards)

Envía contactos, con { name, phone }.

sendMentionAll(chat, text)

Menciona a todo el grupo.

sendGroupInvite(to, group, caption)

Envía una invitación.

setMemberLabel(group, label)

Pone tu etiqueta en un grupo.

sendProto(chat, bytes)

Envía cualquier mensaje protobuf.

typing(chat, state)

composing, recording o paused.

setOnline(available)

Aparece en línea o no.

groupInfo(chat)

Datos de un grupo.

joinedGroups()

Tus grupos.

onWhatsApp(...phones)

Comprueba números.

profilePicture(jid, fullSize)

URL de la foto de perfil.

block(jid)

Bloquea.

unblock(jid)

Desbloquea.

modifyChat(chat, action, seconds)

Archiva, fija, silencia y más.

labelChat(chat, labelId, labeled)

Etiqueta un chat.

labelMessage(chat, message, labelId, labeled)

Etiqueta un mensaje.

editLabel(id, name, color)

Crea o edita una etiqueta.

deleteLabel(id)

Borra una etiqueta.

saveQuickReply(shortcut, text)

Guarda una respuesta rápida.

chats()

Tus chats, del store de chats. No es asíncrono.

contacts()

Tus contactos. No es asíncrono.

messages(chat, { limit, before })

Mensajes de un chat. No es asíncrono.

botList()

Bots de Meta AI.

reachoutTimelock()

Si WhatsApp limitó el contacto con gente nueva.

newChatMessageCap()

Cupo de chats nuevos.

disappearingModes(...jids)

Mensajes temporales de cada chat.

Funciones del paquete#

FunciónQué hace
importBaileys(source, target)Migra una sesión de Baileys
credits(), version()Créditos y versión
NativeClientEl cliente de bajo nivel que usa SoblendWA

Stores#

Además de los stores incluidos, puedes escribir el tuyo en JavaScript.

Lenguajes

Python

El motor en Rust como módulo nativo para asyncio, con el trabajo pesado fuera del GIL.

Los métodos son corrutinas normales de Python: se usan con await, sin hilos ni callbacks.

Crear el cliente#

import asyncio
from soblendwa import SoblendWA


async def main():
    bot = await SoblendWA.create(store={"type": "sqlite", "path": "session/auth.db"}, push_name="Mi Bot")

    @bot.on("qr")
    def qr(event):
        print("Escanea:", event["code"])

    @bot.on("message")
    async def answer(message):
        if message["info"]["fromMe"]:
            return
        if message["text"] == "!ping":
            await bot.reply(message, f"Pong\n{bot.elapsed(message):.2f} ms")

    await bot.run()


asyncio.run(main())

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}.

Eventos#

@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"].

Métodos#

chat acepta un JID completo o solo el número. message acepta el diccionario recibido o su id.

connect()

Conecta.

run()

Conecta y atiende los eventos.

disconnect()

Cierra la conexión y conserva la sesión.

logout()

Desvincula y borra la sesión.

bot.me

Tu JID, o None.

bot.connected

Si está conectado.

ping()

Latencia de red en milisegundos.

stats()

Métricas. No es corrutina.

pending_sends()

Envíos esperando confirmación.

elapsed(message)

Milisegundos que lleva el mensaje dentro del bot. No es corrutina.

request_pair_code(phone)

Pide un código de vinculación.

send_text(chat, text, quoted=None, preview=None)

Envía texto.

reply(message, text)

Responde citando.

react(message, emoji)

Reacciona.

edit(chat, message_id, text)

Edita un mensaje tuyo.

revoke(chat, message_id)

Elimina para todos.

mark_played(message)

Marca una nota de voz como reproducida.

send_image(chat, data, **options)

Envía una imagen.

send_video(chat, data, **options)

Envía un video.

send_audio(chat, data, **options)

Envía audio. Con voice_note=True, como nota de voz.

send_document(chat, data, **options)

Envía un documento.

send_sticker(chat, data, **options)

Envía un sticker WebP.

send_media(chat, kind, data, quoted=None, **options)

Envío genérico.

send_file(chat, kind, path, quoted=None, **options)

Envía un archivo del disco, por bloques.

download(message)

Descarga y descifra. Devuelve bytes.

download_to_file(message, path)

Descarga a disco y devuelve los bytes escritos.

send_interactive(chat, body, buttons, quoted=None, **header)

Mensaje interactivo. header admite title, subtitle y footer.

send_buttons(chat, text, buttons, footer=None)

Botones simples, como pares (id, texto).

send_list(chat, title, text, button_text, sections, footer=None)

Lista de opciones.

send_poll(chat, name, options, selectable=1)

Crea una encuesta.

vote_poll(chat, poll_id, options)

Vota.

send_contacts(chat, cards)

Envía contactos, como pares (nombre, teléfono).

send_mention_all(chat, text)

Menciona a todo el grupo.

send_group_invite(to, group, caption=None)

Envía una invitación.

set_member_label(group, label)

Pone tu etiqueta en un grupo.

send_proto(chat, proto)

Envía cualquier mensaje protobuf.

typing(chat, state="composing")

composing, recording o paused.

set_online(available=True)

Aparece en línea o no.

group_info(chat)

Datos de un grupo.

joined_groups()

Tus grupos.

on_whatsapp(*phones)

Comprueba números.

profile_picture(jid, full_size=False)

URL de la foto de perfil.

block(jid)

Bloquea.

unblock(jid)

Desbloquea.

modify_chat(chat, action, seconds=None)

Archiva, fija, silencia y más.

label_chat(chat, label_id, labeled=True)

Etiqueta un chat.

label_message(chat, message, label_id, labeled=True)

Etiqueta un mensaje.

edit_label(id, name, color=0)

Crea o edita una etiqueta.

delete_label(id)

Borra una etiqueta.

save_quick_reply(shortcut, text)

Guarda una respuesta rápida.

chats()

Tus chats, del store de chats. No es corrutina.

contacts()

Tus contactos. No es corrutina.

messages(chat, limit=50, before=None)

Mensajes de un chat. No es corrutina.

bot_list()

Bots de Meta AI.

reachout_timelock()

Si WhatsApp limitó el contacto con gente nueva.

new_chat_message_cap()

Cupo de chats nuevos.

disappearing_modes(*jids)

Mensajes temporales de cada chat.

Funciones del paquete#

FunciónQué hace
import_baileys(source, **target)Migra una sesión de Baileys. Por defecto, a la carpeta session
credits()Créditos
__version__, __author__Versión y autor
NativeClientEl cliente de bajo nivel que usa SoblendWA

Lenguajes

Cliente JavaScript

soblendwa-client habla con el servidor por WebSocket. Sin nada nativo, sin dependencias y con tipos.

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 ws.

Conectar#

import { connect } from 'soblendwa-client'

const bot = await connect('ws://127.0.0.1:8787', { session: 'ventas', token: 'mi-clave' })

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`)
  }
})
OpciónPor defectoPara qué
sessiondefaultLa cuenta. El servidor la abre si no existe
tokenNingunoLa clave del servidor
reconnecttrueReconectar solo
timeout120.000 msTiempo máximo de cada llamada

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.

Eventos#

bot.on('qr', ({ code }) => console.log(code))
bot.on('connected', () => console.log('En línea'))
bot.on('lagged', (missed) => console.warn('Perdí', missed, 'eventos'))
bot.on('error', (error) => console.error(error))

async function recorrer() {
  for await (const event of bot.events()) {
    console.log(event.type)
  }
}

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.

Varias cuentas#

Cada sesión es una cuenta distinta, y el servidor las maneja todas a la vez.

const ventas = await connect(url, { session: 'ventas', token })
const soporte = await connect(url, { session: 'soporte', token })

Métodos#

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.

const estado = await bot.status()
await bot.read(message)
const resultado = await bot.call('sendText', { chat: '5215512345678', text: 'hola' })
bot.close()

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.

Arrancarlo#

Terminal
soblendwa-server --token mi-clave --session ventas --session soporte

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.

Opciones#

OpciónVariable de entornoPor defectoPara qué
--hostSOBLENDWA_HOST127.0.0.1Dónde escucha
--portSOBLENDWA_PORT8787Puerto
--tokenSOBLENDWA_TOKENNingunoClave obligatoria para WebSocket, HTTP y webhooks
--dirSOBLENDWA_DIRsessionsUna carpeta por cuenta
--sqliteSOBLENDWA_SQLITE—Una base SQLite para todas las cuentas
--mongoSOBLENDWA_MONGO—MongoDB Experimental
--mongo-dbSOBLENDWA_MONGO_DBsoblendwaNombre de la base de MongoDB
--memory——Todo en memoria
--session—defaultCuentas que se abren al arrancar. Se repite. Las demás se abren al primer uso
--webhookSOBLENDWA_WEBHOOKNingunoEnvía cada evento por POST a tu URL
--proxySOBLENDWA_PROXYNingunoProxy HTTP o SOCKS5
--push-nameSOBLENDWA_PUSH_NAMENingunoNombre visible
--no-chat-store——Desactiva el store de chats
--no-link-previews——Desactiva las vistas previas
--fake——Usa el WhatsApp falso

El nombre de una sesión tiene de 1 a 64 caracteres: letras, números, - y _. Los registros se ajustan con RUST_LOG.

Autenticación#

Con --token, cada petición debe llevar la clave de una de estas formas:

  • La cabecera Authorization: Bearer mi-clave.
  • El parámetro ?token=mi-clave.

Sin clave o con una incorrecta, responde 401 con {"error":{"message":"missing or wrong token"}}.

Rutas HTTP#

RutaQué hace
GET /Nombre, versión, sesiones y métodos disponibles. No pide clave
GET /sessionsLas cuentas abiertas
GET /api/<cuenta>El estado de esa cuenta, con el QR pendiente si lo hay. La abre si no existe
POST /api/<cuenta>/<método>Llama a un método, con los parámetros como JSON en el cuerpo
GET /ws/<cuenta>WebSocket de esa cuenta
GET /wsWebSocket de la cuenta default
Enviar un texto
curl -X POST http://127.0.0.1:8787/api/ventas/sendText \
  -H "Authorization: Bearer mi-clave" \
  -d '{"chat":"5215512345678","text":"Hola desde curl"}'
RespuestaCuándo
200 con {"result": …}Funcionó
400 con {"error":{"message":…}}El cuerpo no es JSON o el nombre de la cuenta no es válido
401Falta la clave o está mal
422 con {"error":{"message":…}}El método falló. El mensaje explica por qué

Protocolo WebSocket#

Al conectar a ws://host:8787/ws/<cuenta>?token=… llega primero el saludo y, si hay un QR pendiente, el evento qr.

Saludo
{ "type": "hello", "session": "ventas", "me": "5215512345678:3@s.whatsapp.net", "connected": true, "methods": ["status", "sendText"] }

Después llegan los eventos:

Evento
{ "session": "ventas", "event": { "type": "message", "text": "hola" } }

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.

Llamada
{ "id": 1, "method": "sendText", "params": { "chat": "5215512345678", "text": "hola" } }
Respuesta correcta
{ "id": 1, "result": "3EB0ABC123" }
Respuesta con error
{ "id": 1, "error": { "message": "missing parameter \"text\"" } }

Si un cliente va tan lento que el servidor descarta eventos, recibe {"type":"lagged","missed":N}. El búfer guarda hasta 4.096 eventos por cuenta.

Webhooks#

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.

Recibir webhooks en Node.js
const http = require('node:http')

http.createServer((request, response) => {
  if (request.headers.authorization !== 'Bearer mi-clave') {
    response.writeHead(401).end()
    return
  }
  let body = ''
  request.on('data', (chunk) => (body += chunk))
  request.on('end', () => {
    const { session, event } = JSON.parse(body)
    console.log(session, event.type)
    response.writeHead(200).end()
  })
}).listen(3000)

Métodos disponibles#

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.

MétodoParámetrosDevuelve
status—{ me, connected }
connect, disconnect, logout——
ping—Milisegundos
stats—Métricas
pendingSends—[{ id, chat, queuedAt, attempts }]
elapsedmessageIdMilisegundos
requestPairCodephoneEl código
sendTextchat, text, quotedId?, preview?id
replymessageId, textid
reactmessageId, emojiid
read, markPlayedmessageId—
sendMediachat, kind, data, y las opciones de multimedia, con quotedId?id
sendFilechat, kind, path, y las mismas opcionesid
downloadmessageId{ base64, size }
downloadToFilemessageId, pathBytes escritos
sendPollchat, name, options, selectable?id
votePollchat, pollId, optionsid
editchat, messageId, textid
revokechat, messageIdid
typingchat, state?—
setOnlineavailable?—
sendInteractivechat, body, buttons, title?, subtitle?, footer?, quotedId?id
sendButtonschat, text, buttons, footer?id
sendListchat, title, text, buttonText, sections, footer?id
sendContactschat, cardsid
sendMentionAllchat, textid
sendGroupInviteto, group, caption?id
setMemberLabelgroup, labelid
sendProtochat, protoid
groupInfochatDatos del grupo
joinedGroups—Lista de grupos
onWhatsAppphones[{ query, jid, exists, lid }]
profilePicturejid, fullSize?URL o null
block, unblockjid—
modifyChatchat, action, seconds?—
labelChatchat, labelId, labeled?—
labelMessagechat, messageId, labelId, labeled?—
editLabelid, name, color?—
deleteLabelid—
saveQuickReplyshortcut, textid
chats, contacts—Lista
messageschat, limit?, before?Lista
searchtext, limit?Lista
botList, reachoutTimelock, newChatMessageCap—Datos de la cuenta
disappearingModesjids[{ jid, seconds, setAt }]

Ejemplos en otros lenguajes#

import requests

requests.post(
    "http://127.0.0.1:8787/api/ventas/sendText",
    headers={"Authorization": "Bearer mi-clave"},
    json={"chat": "5215512345678", "text": "Hola"},
)

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.

Crear y conectar#

builder() -> ClientBuilder

Empieza a configurar un cliente.

asyncconnect(&self) -> Result<()>

Abre la conexión y vuelve al instante. El resultado llega por eventos. Mantiene la conexión viva y reconecta en segundo plano.

asyncdisconnect(&self)

Cierra la conexión y guarda lo pendiente en el almacenamiento. Conserva la sesión.

asynclogout(&self) -> Result<()>

Le pide a WhatsApp desvincular este dispositivo y borra todos los datos de la sesión.

asyncrequest_pair_code(&self, phone: &str) -> Result<String>

Pide un código de 8 caracteres para vincular sin QR. Llámalo después del primer Event::Qr.

Estado#

is_connected(&self) -> bool

Indica si la sesión está conectada y lista.

is_logged_in(&self) -> bool

Indica si hay una cuenta vinculada.

me(&self) -> Option<Jid>

Tu JID de número, con dispositivo.

lid(&self) -> Option<Jid>

Tu LID.

creds(&self) -> Creds

Una copia de las credenciales. Contiene claves privadas: no las muestres en registros.

config(&self) -> &Config

La configuración con la que se creó.

store(&self) -> &Arc<dyn Store>

El almacenamiento en uso.

chats(&self) -> Option<&Arc<ChatStore>>

El store de chats, si está activado.

stats(&self) -> Stats

Las métricas de la sesión.

asyncpending_sends(&self) -> crate::Result<Vec<PendingSend>>

Los envíos del outbox esperando confirmación.

asyncping(&self) -> Result<Duration>

Mide la latencia de red ahora. Tarda un viaje de ida y vuelta.

latency(&self) -> Option<Duration>

La última latencia medida en segundo plano. Responde al instante.

asyncwa_version(&self) -> [u32; 3]

La versión de WhatsApp Web en uso.

Enviar#

asyncsend_message(&self, chat: &Jid, message: impl Into<Message>) -> Result<String>

Envía cualquier Message. Devuelve el id. Es la base de los demás envíos.

asyncsend_message_with(&self, chat: &Jid, message: Message, options: SendOptions) -> Result<String>

Como send_message, con SendOptions.

asyncsend_text(&self, chat: &Jid, text: impl Into<String>) -> Result<String>

Envía texto. Si trae un enlace, añade la vista previa.

asyncreply(&self, to: &MessageEvent, message: impl Into<Message>) -> Result<String>

Responde a un mensaje, citándolo, en el mismo chat.

asyncreact(&self, to: &MessageEvent, emoji: &str) -> Result<String>

Reacciona a un mensaje. Con un emoji vacío, quita la reacción.

asyncedit_message(&self, chat: &Jid, id: &str, replacement: impl Into<Message>) -> Result<String>

Reemplaza el contenido de un mensaje tuyo.

asyncrevoke_message(&self, chat: &Jid, key: MessageKey) -> Result<String>

Elimina un mensaje para todos.

asyncpin_message(&self, chat: &Jid, key: MessageKey, duration_secs: u32) -> Result<String>

Fija un mensaje durante los segundos indicados.

asyncunpin_message(&self, chat: &Jid, key: MessageKey) -> Result<String>

Desfija un mensaje.

asyncsend_album(&self, chat: &Jid, items: Vec<Message>) -> Result<Vec<String>>

Envía varias fotos o videos ya preparados como un álbum. Devuelve el id del álbum y el de cada elemento.

asyncsend_status(&self, message: impl Into<Message>, recipients: &[Jid]) -> Result<String>

Publica un estado para esos contactos.

asyncsend_group_invite(&self, to: &Jid, group: &Jid, caption: Option<&str>) -> Result<String>

Envía una tarjeta de invitación a un grupo. Requiere ser administrador.

asyncsend_product(&self, chat: &Jid, owner: &Jid, product: &Product, image: &[u8], body: Option<&str>) -> Result<String>

Envía un producto de tu catálogo. Experimental

asyncset_member_label(&self, group: &Jid, label: &str) -> Result<String>

Pone tu etiqueta en un grupo, de hasta 30 caracteres. Experimental

Vista previa de enlaces#

asyncsend_text_with_preview(&self, chat: &Jid, text: impl Into<String>) -> Result<String>

Envía un texto pidiendo la vista previa de forma explícita.

Multimedia#

asyncsend_media(&self, chat: &Jid, data: &[u8], kind: MediaKind, options: MediaOptions) -> Result<String>

Sube y envía con MediaOptions.

asyncsend_image(&self, chat: &Jid, data: &[u8], caption: Option<&str>) -> Result<String>

Envía una imagen, con caption opcional.

asyncsend_video(&self, chat: &Jid, data: &[u8], caption: Option<&str>) -> Result<String>

Envía un video, con caption opcional.

asyncsend_audio(&self, chat: &Jid, data: &[u8], voice_note: bool) -> Result<String>

Envía audio. Con true, como nota de voz.

asyncsend_document(&self, chat: &Jid, data: &[u8], file_name: &str, mimetype: &str) -> Result<String>

Envía un documento con su nombre y tipo MIME.

asyncsend_sticker(&self, chat: &Jid, webp: &[u8]) -> Result<String>

Envía un sticker en WebP.

asyncprepare_media(&self, data: &[u8], kind: MediaKind, options: MediaOptions) -> Result<Message>

Sube el archivo y devuelve un Message listo para enviar.

asyncupload(&self, data: &[u8], kind: MediaKind) -> Result<UploadedMedia>

Cifra y sube los bytes sin enviarlos. Reutiliza lo ya subido.

asyncdownload(&self, media: &dyn Downloadable) -> Result<Vec<u8>>

Descarga y verifica cualquier multimedia.

asyncdownload_message(&self, message: &Message) -> Result<Vec<u8>>

Descarga la multimedia de un Message.

asyncdownload_event(&self, event: &MessageEvent) -> Result<Vec<u8>>

Descarga la de un mensaje recibido. Si caducó, pide que se vuelva a subir.

asyncreupload_media(&self, event: &MessageEvent) -> Result<Message>

Le pide al remitente que vuelva a subir un archivo caducado. Experimental

asyncforget_upload(&self, kind: MediaKind, file_sha256: &[u8]) -> crate::Result<()>

Olvida un archivo de la caché de multimedia.

Archivos grandes#

asyncsend_media_file(&self, chat: &Jid, path: impl AsRef<Path>, kind: MediaKind, options: MediaOptions) -> Result<String>

Envía un archivo del disco por bloques, con memoria constante.

asyncprepare_media_file(&self, path: impl AsRef<Path>, kind: MediaKind, options: MediaOptions) -> Result<Message>

Como prepare_media, desde una ruta.

asyncupload_file(&self, path: impl AsRef<Path>, kind: MediaKind) -> Result<UploadedMedia>

Sube un archivo del disco por bloques.

asyncupload_reader<R>(&self, reader: R, kind: MediaKind) -> Result<UploadedMedia>

Sube desde un lector de un solo uso, cifrando hacia un archivo temporal.

asyncdownload_to_file(&self, media: &dyn Downloadable, path: impl AsRef<Path>) -> Result<u64>

Descarga a disco por bloques. El archivo solo aparece con su nombre si pasa la verificación.

asyncdownload_event_to_file(&self, event: &MessageEvent, path: impl AsRef<Path>) -> Result<u64>

Como download_to_file, para un mensaje recibido. Si caducó, pide que se vuelva a subir.

asyncdownload_to_writer<W>(&self, media: &dyn Downloadable, writer: &mut W) -> Result<u64>

Descarga hacia cualquier escritor. Devuelve los bytes escritos.

Recibos#

asyncread_message(&self, event: &MessageEvent) -> Result<()>

Marca un mensaje recibido como leído.

asyncmark_read(&self, chat: &Jid, sender: Option<&Jid>, ids: &[String]) -> Result<()>

Marca varios mensajes de un chat como leídos.

asyncmark_played(&self, event: &MessageEvent) -> Result<()>

Marca una nota de voz o un video circular como reproducidos.

asyncsend_receipt(&self, chat: &Jid, sender: Option<&Jid>, ids: &[String], kind: &str) -> Result<()>

Envía un recibo de cualquier tipo.

Encuestas y eventos#

asyncvote_poll(&self, chat: &Jid, poll_id: &str, options: &[&str]) -> Result<String>

Vota en una encuesta con las opciones elegidas, por nombre.

asyncrespond_event(&self, chat: &Jid, event_id: &str, going: EventResponseKind, extra_guests: u32) -> Result<String>

Responde a un evento de calendario, con invitados extra opcionales.

Grupos#

asyncgroup_info(&self, group: &Jid) -> Result<GroupInfo>

Datos de un grupo, desde la caché si los tiene.

asyncgroup_metadata(&self, group: &Jid) -> Result<GroupInfo>

Datos de un grupo, siempre desde el servidor.

asyncjoined_groups(&self) -> Result<Vec<GroupInfo>>

Todos tus grupos.

asynccreate_group(&self, subject: &str, participants: &[Jid]) -> Result<GroupInfo>

Crea un grupo.

asyncupdate_participants(&self, group: &Jid, participants: &[Jid], action: ParticipantAction) -> Result<Vec<ParticipantResult>>

Añade, saca, nombra o quita administradores. Devuelve el resultado de cada persona.

asyncset_group_subject(&self, group: &Jid, subject: &str) -> Result<()>

Cambia el nombre.

asyncset_group_description(&self, group: &Jid, description: Option<&str>) -> Result<()>

Cambia la descripción. Con None, la quita.

asyncset_group_announce(&self, group: &Jid, admins_only: bool) -> Result<()>

Decide si solo los administradores pueden escribir.

asyncset_group_locked(&self, group: &Jid, locked: bool) -> Result<()>

Decide si solo los administradores pueden editar los datos.

asyncset_group_ephemeral(&self, group: &Jid, seconds: u32) -> Result<()>

Activa los mensajes temporales. 0 los desactiva.

asyncset_group_picture(&self, group: &Jid, image: Vec<u8>) -> Result<()>

Cambia la foto. La imagen se recorta y redimensiona sola.

asyncset_group_join_approval(&self, group: &Jid, enabled: bool) -> Result<()>

Decide si las solicitudes para entrar necesitan aprobación.

asyncset_group_member_add_mode(&self, group: &Jid, all_members: bool) -> Result<()>

Decide si todos los miembros pueden añadir gente.

asyncleave_group(&self, group: &Jid) -> Result<()>

Sale del grupo.

asyncgroup_invite_code(&self, group: &Jid) -> Result<String>

El código de invitación.

asyncrevoke_group_invite(&self, group: &Jid) -> Result<String>

Invalida el código y devuelve uno nuevo.

asyncjoin_group(&self, invite: &str) -> Result<Jid>

Entra con un código o enlace de invitación.

asyncgroup_invite_info(&self, invite: &str) -> Result<GroupInfo>

Datos de un grupo a partir de su invitación, sin entrar.

asyncaccept_group_invite(&self, from: &Jid, invite: &GroupInviteMessage) -> Result<Jid>

Acepta una tarjeta de invitación recibida por mensaje.

asyncgroup_join_requests(&self, group: &Jid) -> Result<Vec<JoinRequest>>

Las solicitudes pendientes.

asyncrespond_join_requests(&self, group: &Jid, participants: &[Jid], approve: bool) -> Result<Vec<ParticipantResult>>

Aprueba o rechaza solicitudes.

Comunidades Experimental#

asynccreate_community(&self, subject: &str, description: &str) -> Result<GroupInfo>

Crea una comunidad.

asynccreate_community_group(&self, community: &Jid, subject: &str, participants: &[Jid]) -> Result<GroupInfo>

Crea un grupo dentro de una comunidad.

asynccommunity_groups(&self, community: &Jid) -> Result<Vec<LinkedGroup>>

Los grupos de una comunidad.

asyncleave_community(&self, community: &Jid) -> Result<()>

Sale de la comunidad.

asyncset_community_join_approval(&self, community: &Jid, enabled: bool) -> Result<()>

Decide si entrar necesita aprobación.

Canales#

asyncnewsletter_create(&self, name: &str, description: Option<&str>) -> Result<NewsletterInfo>

Crea un canal. Experimental

asyncnewsletter_info(&self, jid: &Jid) -> Result<NewsletterInfo>

Datos de un canal por su JID.

asyncnewsletter_info_by_invite(&self, invite: &str) -> Result<NewsletterInfo>

Datos de un canal por su enlace o código.

asyncnewsletter_follow(&self, jid: &Jid) -> Result<()>

Sigue un canal.

asyncnewsletter_unfollow(&self, jid: &Jid) -> Result<()>

Deja de seguirlo.

asyncnewsletter_mute(&self, jid: &Jid, muted: bool) -> Result<()>

Lo silencia o no.

asyncnewsletter_update(&self, jid: &Jid, name: Option<&str>, description: Option<&str>, picture: Option<Vec<u8>>) -> Result<()>

Cambia el nombre, la descripción o la foto. Experimental

asyncnewsletter_delete(&self, jid: &Jid) -> Result<()>

Borra un canal tuyo. Experimental

asyncnewsletter_subscribers(&self, jid: &Jid) -> Result<u64>

Número de suscriptores.

asyncnewsletter_admin_count(&self, jid: &Jid) -> Result<u64>

Número de administradores.

asyncnewsletter_change_owner(&self, jid: &Jid, new_owner: &Jid) -> Result<()>

Cambia el dueño. Experimental

asyncnewsletter_demote(&self, jid: &Jid, admin: &Jid) -> Result<()>

Le quita el rol de administrador a alguien. Experimental

asyncnewsletter_react(&self, jid: &Jid, server_id: &str, reaction: Option<&str>) -> Result<()>

Reacciona a una publicación por su server_id. Con None, quita la reacción.

asyncnewsletter_messages(&self, jid: &Jid, count: u32, before: Option<u64>) -> Result<Vec<NewsletterMessage>>

Lee publicaciones, opcionalmente anteriores a un server_id.

asyncnewsletter_live_updates(&self, jid: &Jid) -> Result<Option<u64>>

Pide recibir los cambios en vivo. Devuelve por cuántos segundos, si WhatsApp lo indica.

Chats y app state#

asyncarchive_chat(&self, chat: &Jid, archived: bool) -> Result<()>

Archiva o desarchiva.

asyncpin_chat(&self, chat: &Jid, pinned: bool) -> Result<()>

Fija o desfija.

asyncmute_chat(&self, chat: &Jid, duration: Option<std::time::Duration>) -> Result<()>

Silencia por ese tiempo. Con None, quita el silencio.

asyncmark_chat_read(&self, chat: &Jid, read: bool) -> Result<()>

Marca el chat como leído o no leído.

asyncdelete_chat(&self, chat: &Jid) -> Result<()>

Borra el chat.

asyncclear_chat(&self, chat: &Jid) -> Result<()>

Vacía el chat.

asyncstar_message(&self, chat: &Jid, message_id: &str, from_me: bool, starred: bool) -> Result<()>

Destaca un mensaje o le quita el destacado.

asyncsave_contact(&self, jid: &Jid, full_name: &str) -> Result<()>

Guarda un contacto.

asyncchat_modify(&self, chat: &Jid, modification: ChatModification) -> Result<()>

Aplica cualquier ChatModification.

asyncset_push_name(&self, name: &str) -> Result<()>

Cambia tu nombre visible.

asyncedit_label(&self, id: &str, name: &str, color: i32) -> Result<()>

Crea o edita una etiqueta. Experimental

asyncdelete_label(&self, id: &str) -> Result<()>

Borra una etiqueta. Experimental

asynclabel_chat(&self, chat: &Jid, label_id: &str, labeled: bool) -> Result<()>

Pone o quita una etiqueta en un chat. Experimental

asynclabel_message(&self, chat: &Jid, message_id: &str, label_id: &str, labeled: bool) -> Result<()>

Pone o quita una etiqueta en un mensaje. Experimental

asyncsave_quick_reply(&self, id: Option<&str>, shortcut: &str, message: &str) -> Result<String>

Crea o edita una respuesta rápida. Devuelve su id. Experimental

asyncdelete_quick_reply(&self, id: &str) -> Result<()>

Borra una respuesta rápida. Experimental

asyncsync_app_state(&self, collections: &[&str], initial: bool) -> Result<()>

Fuerza la sincronización de esas colecciones.

Historial#

asyncrequest_history(&self, chat: &Jid, oldest: &MessageKey, oldest_timestamp_ms: i64, count: i32) -> Result<String>

Le pide al teléfono hasta 50 mensajes anteriores. Llegan como Event::HistorySync.

asyncrequest_placeholder_resend(&self, keys: Vec<MessageKey>) -> Result<String>

Le pide al teléfono una copia de mensajes que no se pudieron descifrar.

asyncdownload_history(&self, notification: &HistorySyncNotification) -> Result<HistorySync>

Descarga y descomprime un bloque de historial.

Perfil y privacidad#

asyncprofile_picture_url(&self, jid: &Jid, full_size: bool) -> Result<Option<String>>

URL de una foto de perfil. Con true, en tamaño completo.

asyncset_profile_picture(&self, target: Option<&Jid>, image: Vec<u8>) -> Result<()>

Cambia tu foto, o la de un grupo si indicas su JID.

asyncremove_profile_picture(&self, target: Option<&Jid>) -> Result<()>

Quita esa foto.

asyncset_about(&self, text: &str) -> Result<()>

Cambia tu información.

asyncfetch_about(&self, jids: &[Jid]) -> Result<Vec<About>>

Lee la información de varias cuentas.

asyncblock(&self, jid: &Jid) -> Result<()>

Bloquea una cuenta.

asyncunblock(&self, jid: &Jid) -> Result<()>

Desbloquea una cuenta.

asyncblocklist(&self) -> Result<Vec<Jid>>

Tu lista de bloqueados.

asyncprivacy_settings(&self) -> Result<HashMap<String, String>>

Tus ajustes de privacidad.

asyncset_privacy(&self, setting: PrivacySetting, value: PrivacyValue) -> Result<()>

Cambia un ajuste de privacidad.

asyncset_default_disappearing(&self, seconds: u32) -> Result<()>

Duración por defecto de los mensajes temporales. 0 los desactiva.

asyncset_disappearing(&self, chat: &Jid, seconds: u32) -> Result<()>

Duración de los mensajes temporales en un chat.

asyncdisappearing_modes(&self, jids: &[Jid]) -> Result<Vec<DisappearingMode>>

Los mensajes temporales de varios chats.

asyncbusiness_profile(&self, jid: &Jid) -> Result<Option<BusinessProfile>>

El perfil de empresa de una cuenta, si tiene.

Cuentas#

asyncon_whatsapp(&self, phones: &[&str]) -> Result<Vec<OnWhatsApp>>

Comprueba si unos números tienen WhatsApp.

asynclid_for_phone(&self, pn: &Jid) -> Result<Option<Jid>>

El LID de un número.

asyncphone_for_lid(&self, lid: &Jid) -> Result<Option<Jid>>

El número de un LID, si WhatsApp lo compartió.

asyncbot_list(&self) -> Result<Vec<BotInfo>>

Los bots de Meta AI disponibles.

asyncreachout_timelock(&self) -> Result<ReachoutTimelock>

Si WhatsApp limitó el contacto con gente nueva.

asyncnew_chat_message_cap(&self) -> Result<Value>

Cuántos chats nuevos puedes iniciar, como JSON.

Presencia y llamadas#

asyncset_presence(&self, available: bool) -> Result<()>

Aparece en línea o no.

asyncsubscribe_presence(&self, jid: &Jid) -> Result<()>

Pide recibir la presencia de una cuenta.

asyncsend_chat_presence(&self, chat: &Jid, state: ChatPresence) -> Result<()>

Envía escribiendo, grabando o pausa.

asyncreject_call(&self, call_id: &str, caller: &Jid) -> Result<()>

Rechaza una llamada entrante.

Empresa Experimental#

asynccatalog(&self, jid: Option<&Jid>, limit: u32, cursor: Option<&str>) -> Result<Catalog>

Una página de un catálogo.

asynccatalog_collections(&self, jid: Option<&Jid>, limit: u32) -> Result<Vec<ProductCollection>>

Las colecciones de un catálogo.

asyncorder_details(&self, order_id: &str, token: &str) -> Result<Order>

Los datos de un pedido.

asyncproduct_create(&self, draft: &ProductDraft) -> Result<Product>

Crea un producto.

asyncproduct_update(&self, product_id: &str, draft: &ProductDraft) -> Result<Product>

Edita un producto.

asyncproduct_delete(&self, product_ids: &[&str]) -> Result<u32>

Borra productos. Devuelve cuántos.

asyncupdate_business_profile(&self, update: &BusinessProfileUpdate) -> Result<()>

Cambia tu perfil de empresa.

asyncset_business_cover_photo(&self, image: Vec<u8>) -> Result<String>

Pone una foto de portada. Devuelve su id.

asyncremove_business_cover_photo(&self, id: &str) -> Result<()>

Quita la foto de portada.

Bajo nivel#

Para lo que la librería aún no cubre. Un error aquí puede hacer que WhatsApp cierre la conexión.

send_node(&self, node: &Node) -> Result<()>

Envía un nodo binario sin esperar respuesta.

asyncquery(&self, node: Node) -> Result<Node>

Envía un nodo y espera la respuesta. Añade un id si no lo tiene.

generate_tag(&self) -> String

Genera un id para un nodo.

asyncrotate_signed_pre_key(&self) -> Result<()>

Rota la signed pre-key. Normalmente ocurre sola, cada 30 días.

asyncclean_dirty_bits(&self, kind: &str, since: Option<i64>) -> Result<()>

Le avisa a WhatsApp que ya atendiste una señal de sincronización.

Referencia de la API

ClientBuilder

Configura el cliente antes de crearlo. Los valores por defecto están en Configuración.

store(self, store: Arc<dyn Store>) -> Self

Dónde se guarda la sesión. Por defecto, memoria.

chat_store(self, chats: Arc<ChatStore>) -> Self

Activa el store de chats.

config(self, config: Config) -> Self

Reemplaza toda la configuración.

device_name(self, name: impl Into<String>) -> Self

Nombre en Dispositivos vinculados.

platform(self, platform: Platform) -> Self

La plataforma que se informa a WhatsApp.

version(self, version: [u32; 3]) -> Self

Fija la versión de WhatsApp Web.

push_name(self, name: impl Into<String>) -> Self

Tu nombre visible.

country_code(self, code: impl Into<String>) -> Self

El país que se informa a WhatsApp.

online_on_connect(self, online: bool) -> Self

Aparecer en línea al conectar.

auto_reconnect(self, enabled: bool) -> Self

Reconectar solo.

query_timeout(self, timeout: Duration) -> Self

Tiempo máximo de una consulta.

media_cache(self, ttl: Option<Duration>) -> Self

Cuánto se recuerda la multimedia. Con None, se desactiva.

send_limit(self, limit: Option<SendLimit>) -> Self

El límite de envíos. Con None, se desactiva.

outbox(self, enabled: bool) -> Self

El outbox persistente.

proxy(self, proxy: Proxy) -> Self

El proxy.

workers(self, workers: usize) -> Self

Cuántos mensajes se procesan en paralelo.

ws_url(self, url: impl Into<String>) -> Self

Conecta a otro servidor, como uno falso.

media_endpoint(self, origin: impl Into<String>) -> Self

Otro servidor de multimedia.

trust_root(self, key: [u8; 32]) -> Self

Raíz de certificados de ese servidor.

asyncbuild(self) -> Result<(Client, EventStream)>

Crea el cliente y devuelve también el canal de eventos.

Referencia de la API

Message

Constructores y ayudas de Message, el protobuf de WhatsApp. Sus campos son públicos, así que puedes construir cualquier mensaje.

Message admite From<&str> y From<String>, por eso client.reply(&msg, "hola") funciona.

Crear#

text(text: impl Into<String>) -> Self

Un texto.

reaction(key: MessageKey, emoji: impl Into<String>) -> Self

Una reacción a un mensaje por su clave.

location(latitude: f64, longitude: f64) -> Self

Una ubicación.

contact(display_name: impl Into<String>, vcard: impl Into<String>) -> Self

Un contacto, con su vCard.

contacts<I, A, B>(display_name: impl Into<String>, cards: I) -> Self

Varios contactos.

vcard(name: &str, phone: &str) -> String

Genera el texto vCard de un contacto.

poll<I, S>(name: impl Into<String>, options: I, selectable: u32) -> Self

Una encuesta. Con selectable igual a 1, de opción única.

announcement_poll<I, S>(name: impl Into<String>, options: I, selectable: u32) -> Self

Una encuesta para grupos de anuncios.

event(name: impl Into<String>, start_time_secs: i64) -> Self

Un evento de calendario.

event_details(self, description: Option<&str>, end_time_secs: Option<i64>, location: Option<(f64, f64, &str)>, join_link: Option<&str>, extra_guests_allowed: bool) -> Self

Añade descripción, fin, lugar, enlace e invitados a un evento.

cancel_event(self) -> Self

Marca un evento como cancelado.

group_invite(group: &Jid, subject: impl Into<String>, invite_code: impl Into<String>, expiration: i64, caption: Option<&str>) -> Self

Una tarjeta de invitación con su código. send_group_invite lo hace por ti.

product(product: ProductSnapshot, business_owner: &Jid, body: Option<&str>, footer: Option<&str>) -> Self

Un mensaje de producto.

share_phone_number() -> Self

Comparte tu número en el chat.

request_phone_number() -> Self

Pide el número de la otra persona.

limit_sharing(limited: bool) -> Self

Activa o desactiva el límite de reenvío.

member_label(label: &str) -> Self

Una etiqueta de miembro, de hasta 30 caracteres.

video_note(video: Message) -> Self

Convierte un video preparado en video circular.

pin(key: MessageKey, pinned: bool, duration_secs: u32) -> Self

Fija o desfija un mensaje por su clave.

revoke(key: MessageKey) -> Self

Elimina un mensaje por su clave.

edit(key: MessageKey, replacement: Message) -> Self

Edita un mensaje por su clave.

Interactivos#

buttons<I, A, B>(text: impl Into<String>, footer: Option<&str>, buttons: I) -> Self

Botones simples, en formato native flow.

list(title: impl Into<String>, text: impl Into<String>, button_text: impl Into<String>, footer: Option<&str>, sections: &[Section]) -> Self

Una lista de opciones, en formato native flow.

template(text: impl Into<String>, footer: Option<&str>, buttons: &[TemplateButton]) -> Self

Un mensaje de plantilla.

legacy_buttons<I, A, B>(text: impl Into<String>, footer: Option<&str>, buttons: I) -> Self

El formato viejo de botones. WhatsApp ya no lo muestra en cuentas normales.

legacy_list(title: impl Into<String>, text: impl Into<String>, button_text: impl Into<String>, footer: Option<&str>, sections: &[Section]) -> Self

El formato viejo de listas. WhatsApp ya no lo muestra en cuentas normales.

button_reply(id: impl Into<String>, text: impl Into<String>) -> Self

Una respuesta a un botón.

template_reply(id: impl Into<String>, text: impl Into<String>, index: u32) -> Self

Una respuesta a una plantilla.

list_reply(title: impl Into<String>, row_id: impl Into<String>) -> Self

Una respuesta a una lista.

Modificar#

reply_to(self, quoted: &MessageEvent) -> Self

Cita el mensaje indicado.

mention<'a>(self, jids: impl IntoIterator<Item = &'a Jid>) -> Self

Añade los JID mencionados.

mention_all(self) -> Self

Menciona a todo el grupo.

forwarded(self) -> Self

Lo marca como reenviado.

context_info_mut(&self) -> &mut ContextInfo

Acceso de escritura al ContextInfo.

Leer#

get_text(&self) -> Option<&str>

El texto o el caption, sin importar el tipo.

unwrapped(&self) -> &Message

El mensaje real, quitando los envoltorios.

media(&self) -> Option<&dyn Downloadable>

La multimedia, lista para descargar.

media_type(&self) -> Option<&'static str>

El tipo de multimedia.

stanza_type(&self) -> &'static str

El tipo de mensaje.

is_view_once(&self) -> bool

Si es de una sola vista.

has_content(&self) -> bool

Si tiene algún contenido.

context_info(&self) -> Option<&ContextInfo>

El ContextInfo: lo citado y las menciones.

interactive_reply(&self) -> Option<InteractiveReply>

La opción elegida, si responde a un botón o lista.

Mensajes recibidos#

text(&self) -> Option<&str>

El texto o el caption.

key(&self) -> MessageKey

La clave, para editar, borrar o fijar.

chat(&self) -> &Jid

El chat.

sender(&self) -> &Jid

El remitente.

is_reaction(&self) -> bool

Si es una reacción.

quoted(&self) -> Option<&proto::Message>

El mensaje citado.

mentions(&self) -> Vec<Jid>

Los JID mencionados.

to_json(&self) -> Value

El mensaje en JSON, como en los demás lenguajes.

elapsed(&self) -> Duration

Cuánto lleva dentro del bot.

is_older_than(&self, age: Duration) -> bool

Si es más viejo que esa duración.

is_stale(&self, age: Duration) -> bool

Si llegó con el bot apagado o es más viejo que esa duración.

key(&self) -> MessageKey

La clave del mensaje.

to_json(&self) -> Value

Los datos en JSON.

Botones#

new(body: impl Into<String>) -> Self

Empieza un mensaje interactivo con su texto.

title(self, text: impl Into<String>) -> Self

Añade el título.

subtitle(self, text: impl Into<String>) -> Self

Añade el subtítulo.

image(self, image: ImageMessage) -> Self

Pone una imagen preparada en la cabecera.

video(self, video: VideoMessage) -> Self

Pone un video preparado en la cabecera.

document(self, document: DocumentMessage) -> Self

Pone un documento preparado en la cabecera.

button(self, button: NativeButton) -> Self

Añade cualquier NativeButton.

quick_reply(self, id: impl Into<String>, text: impl Into<String>) -> Self

Añade una respuesta rápida.

url(self, text: impl Into<String>, url: impl Into<String>) -> Self

Añade un botón que abre un enlace.

copy(self, text: impl Into<String>, code: impl Into<String>) -> Self

Añade un botón que copia un código.

call(self, text: impl Into<String>, phone: impl Into<String>) -> Self

Añade un botón de llamada.

select(self, title: impl Into<String>, sections: Vec<Section>) -> Self

Añade un menú de opciones.

build(self) -> Message

Construye el Message.

new(title: impl Into<String>) -> Self

Empieza una sección con su título.

row(self, id: impl Into<String>, title: impl Into<String>) -> Self

Añade una fila.

described_row(self, id: impl Into<String>, title: impl Into<String>, description: impl Into<String>) -> Self

Añade una fila con descripción.

Referencia de la API

ChatStore

El store de chats: chats, contactos y mensajes guardados en local.

new() -> Self

Un store solo en memoria.

with_limit(messages_per_chat: usize) -> Self

Un store en memoria con otro límite de mensajes por chat.

asyncopen(path: impl AsRef<Path>) -> Result<Arc<ChatStore>>

Abre o crea el archivo. Guarda hasta 500 mensajes por chat.

asyncopen_with_limit(path: impl AsRef<Path>, messages_per_chat: usize) -> Result<Arc<ChatStore>>

Abre o crea el archivo con otro límite.

asyncsave(&self) -> Result<()>

Escribe a disco ahora, de forma atómica.

asyncsave_to(&self, path: impl AsRef<Path>) -> Result<()>

Escribe en otra ruta.

chats(&self) -> Vec<Chat>

Todos los chats.

chat(&self, jid: &Jid) -> Option<Chat>

Un chat.

unread_total(&self) -> u32

Total de mensajes sin leer.

contacts(&self) -> Vec<Contact>

Todos los contactos.

contact(&self, jid: &Jid) -> Option<Contact>

Un contacto.

name_of(&self, jid: &Jid) -> Option<String>

El mejor nombre conocido de un JID.

messages(&self, chat: &Jid, limit: usize) -> Vec<StoredMessage>

Los últimos mensajes de un chat.

messages_before(&self, chat: &Jid, before: i64, limit: usize) -> Vec<StoredMessage>

Los mensajes anteriores a esa hora.

message(&self, chat: &Jid, id: &str) -> Option<StoredMessage>

Un mensaje por chat e id.

find_message(&self, id: &str) -> Option<StoredMessage>

Un mensaje por id, en todos los chats.

ingest(&self, event: &Event)

Procesa un evento. El cliente lo hace solo si usaste .chat_store(...).

display_name(&self) -> Option<&str>

El mejor nombre del contacto.

text(&self) -> Option<&str>

El texto o el caption.

to_event(&self) -> MessageEvent

Lo convierte en un MessageEvent, por ejemplo para responderlo con reply.

to_json(&self) -> Value

El mensaje en JSON.

Referencia de la API

Jid

La dirección de una cuenta, grupo o canal.

Jid tiene tres campos públicos: user, device y server. También implementa Display y FromStr, así que puedes usar to_string() y parse().

new(user: impl Into<String>, server: impl Into<String>) -> Self

Un JID con esos usuario y servidor.

phone(number: &str) -> Self

Una cuenta por su número. Conserva solo los dígitos.

group(id: impl Into<String>) -> Self

Un grupo por su id.

lid(id: impl Into<String>) -> Self

Una cuenta por su LID.

server() -> Self

El servidor de WhatsApp, s.whatsapp.net.

group_server() -> Self

El servidor de grupos, g.us.

status_broadcast() -> Self

Los estados, status@broadcast.

with_device(&self, device: u16) -> Self

El mismo JID en ese dispositivo.

to_non_device(&self) -> Self

El mismo JID sin dispositivo.

agent(&self) -> u8

El código de tipo de servidor.

is_user(&self) -> bool

Si es una cuenta.

is_pn(&self) -> bool

Si usa número de teléfono.

is_lid(&self) -> bool

Si usa LID.

is_group(&self) -> bool

Si es un grupo.

is_broadcast(&self) -> bool

Si es una lista de difusión o los estados.

is_status_broadcast(&self) -> bool

Si son los estados.

is_newsletter(&self) -> bool

Si es un canal.

is_server(&self) -> bool

Si es un servidor, sin usuario.

same_user(&self, other: &Jid) -> bool

Si es la misma cuenta, ignorando el dispositivo.

signal_address(&self) -> String

La dirección que usa Signal internamente.

parse(input: &str) -> Result<Self, Error>

Lee un JID desde un texto.

Grupos#

is_admin(&self, jid: &Jid) -> bool

Si esa persona es administradora, buscando por número o por LID.

participant(&self, jid: &Jid) -> Option<&GroupParticipant>

Un participante, buscando por número o por LID.

admins(&self) -> impl Iterator<Item = &GroupParticipant>

Los administradores.

is_lid_addressed(&self) -> bool

Si el grupo usa LID.

Proxy#

parse(url: &str) -> Result<Proxy>

Lee una URL de proxy.

kind(&self) -> ProxyKind

Http o Socks5.

host(&self) -> &str

El host.

port(&self) -> u16

El puerto.

Tiempos#

total(&self) -> Duration

La suma de todas las fases.

to_json(&self) -> Value

Las métricas en JSON.

Privacidad#

as_str(self) -> &'static str

El nombre del ajuste en el protocolo.

as_str(self) -> &'static str

El nombre del valor en el protocolo.

Referencia de la API

Almacenamiento

Los constructores de cada store y las funciones de migración.

Stores#

asyncopen(dir: impl AsRef<Path>) -> Result<Arc<Self>>

Abre o crea una carpeta de sesión.

path(&self) -> &Path

La ruta de esa carpeta.

new() -> Self

Un store en memoria.

asyncopen(path: impl AsRef<Path>) -> Result<Self>

Abre o crea un archivo SQLite, para una cuenta.

asyncopen_account(path: impl AsRef<Path>, account: &str) -> Result<Self>

Lo mismo, para esa cuenta. Varias cuentas pueden compartir el archivo.

in_memory() -> Result<Self>

Una base SQLite en memoria.

from_connection(connection: Connection, account: &str) -> Result<Self>

Usa una conexión de rusqlite que ya tengas.

asyncconnect(url: &str) -> Result<Self>

Se conecta a PostgreSQL y crea las tablas si faltan.

asyncconnect_account(url: &str, account: &str) -> Result<Self>

Lo mismo, para esa cuenta.

asyncfrom_client(client: Client, account: &str) -> Result<Self>

Usa un cliente de tokio-postgres que ya tengas.

asyncconnect(url: &str) -> Result<Self>

Se conecta a Redis.

asyncconnect_account(url: &str, account: &str) -> Result<Self>

Lo mismo, para esa cuenta.

asyncconnect(url: &str, database: &str) -> Result<Self>

Se conecta a MongoDB, a esa base de datos.

asyncconnect_account(url: &str, database: &str, account: &str) -> Result<Self>

Lo mismo, para esa cuenta.

asyncfrom_database(database: Database, account: &str) -> Result<Self>

Usa una base de datos del driver de MongoDB que ya tengas.

new(backend: B) -> Self

Envuelve tu Backend, con caché y escrituras agrupadas.

backend(&self) -> &B

Acceso a tu Backend.

Traits#

TraitMétodos obligatoriosOpcionales
Storeload_creds, save_creds, get, set, remove, clearflush, keys
Backendload_creds, save_creds, fetch, applylist

KeyWrite puede ser Set(kind, key, value), Remove(kind, key) o Clear.

Migración#

soblendwa::store::import_baileys(dir, store) -> Result<BaileysImport>

Importa una carpeta de useMultiFileAuthState.

soblendwa::store::creds_from_baileys(value) -> Result<Creds>

Convierte solo las credenciales de Baileys.

soblendwa::store::import_zapo_sqlite(path, session, store) -> Result<ZapoImport>

Importa una sesión de zapo desde SQLite. Solo lee la base. Feature sqlite.

soblendwa::store::export_zapo_sqlite(store, path, session) -> Result<ZapoExport>

Escribe el estado de vuelta en la base de zapo. Feature sqlite.

soblendwa::store::zapo_sessions(path) -> Result<Vec<String>>

Las sesiones que hay en una base de zapo. Feature sqlite.

ZapoMongo::connect(url, database) -> Result<ZapoMongo>

Se conecta a una base de @zapo-js/store-mongo. Features sqlite y mongo. Experimental

ZapoMongo::new(database) -> ZapoMongo

Usa una base de datos del driver que ya tengas.

zapo_mongo.collection_prefix(prefix) -> ZapoMongo

Indica el prefijo de colecciones.

zapo_mongo.sessions() -> Result<Vec<String>>

Las sesiones que hay en esa base.

soblendwa::store::import_zapo_mongo(source, session, store) -> Result<ZapoImport>

Importa una sesión. Solo lee MongoDB. Experimental

soblendwa::store::export_zapo_mongo(store, target, session) -> Result<ZapoExport>

Escribe el estado de vuelta en MongoDB. Experimental

Referencia de la API

FakeWhatsApp

El WhatsApp falso, con el protocolo real. Requiere la feature testing.

asyncstart() -> Result<Self>

Arranca el servidor en un puerto libre.

asyncconnect(&self, phone: &str) -> Result<(Client, EventStream)>

Crea y conecta una cuenta. Devuelve su cliente y sus eventos.

asyncconnect_with(&self, builder: ClientBuilder) -> Result<(Client, EventStream)>

Conecta una cuenta con un ClientBuilder propio.

builder(&self) -> ClientBuilder

Un ClientBuilder ya apuntando a este servidor.

account(&self, phone: &str) -> Creds

Credenciales vinculadas para ese número.

jid(phone: &str) -> Jid

El JID de un número.

url(&self) -> String

La URL del WebSocket.

trust_root(&self) -> [u8; 32]

Su clave raíz, para .trust_root(...).

media_endpoint(&self) -> String

Su servidor de multimedia.

media_uploads(&self) -> u64

Cuántas subidas recibió.

expire_media(&self)

Caduca los archivos subidos.

create_group(&self, subject: &str, creator: &str, members: &[&str]) -> Jid

Crea un grupo. Devuelve su JID.

drop_connections(&self, phone: &str)

Corta la conexión de esa cuenta.

refuse_connections(&self, phone: &str, refuse: bool)

Rechaza o admite sus conexiones.

block(&self, phone: &str, code: u16)

Bloquea la cuenta con ese código.

unblock(&self, phone: &str)

La desbloquea.

hold_acks(&self, phone: &str, hold: bool)

Retiene o libera sus confirmaciones.

push_node(&self, phone: &str, node: Node)

Le envía un nodo cualquiera.

corrupt_next(&self, phone: &str, messages: u32)

Daña sus siguientes mensajes, para probar reintentos.

routed(&self) -> u64

Cuántos mensajes enrutó.

is_online(&self, phone: &str) -> bool

Si esa cuenta está conectada.

Referencia de la API

Bajo nivel

Módulos públicos para casos avanzados: nodos binarios, criptografía e imágenes.

Nodos binarios#

soblendwa::binary contiene el formato con el que WhatsApp envía todo. Úsalo con client.query y client.send_node.

use soblendwa::binary::Node;

let request = Node::new("iq")
    .attr("to", Jid::server())
    .attr("type", "get")
    .attr("xmlns", "w:p")
    .child_node(Node::new("ping"));
let respuesta = client.query(request).await?;
new(tag: impl Into<Str>) -> Self

Un nodo con esa etiqueta.

attr(self, key: impl Into<Str>, value: impl Into<Value>) -> Self

Añade un atributo.

attr_opt<V: Into<Value>>(self, key: impl Into<Str>, value: Option<V>) -> Self

Añade un atributo solo si hay valor.

set_attr(&self, key: impl Into<Str>, value: impl Into<Value>)

Añade un atributo a un nodo existente.

children(self, nodes: impl IntoIterator<Item = Node>) -> Self

Pone varios hijos.

child_node(self, node: Node) -> Self

Añade un hijo.

push_child(&self, node: Node)

Añade un hijo a un nodo existente.

bytes(self, data: impl Into<Vec<u8>>) -> Self

Pone contenido binario.

text(self, text: impl Into<Str>) -> Self

Pone contenido de texto.

get(&self, key: &str) -> Option<&Value>

Lee un atributo.

has(&self, key: &str) -> bool

Si tiene ese atributo.

attr_str(&self, key: &str) -> Option<&str>

Lee un atributo como texto.

attr_string(&self, key: &str) -> Option<String>

Lee un atributo como String.

attr_jid(&self, key: &str) -> Option<Jid>

Lee un atributo como JID.

attr_u64(&self, key: &str) -> Option<u64>

Lee un atributo como número sin signo.

attr_i64(&self, key: &str) -> Option<i64>

Lee un atributo como número con signo.

attr_is(&self, key: &str, expected: &str) -> bool

Si el atributo tiene ese valor.

nodes(&self) -> &[Node]

Los hijos.

into_nodes(self) -> Vec<Node>

Los hijos, consumiendo el nodo.

child(&self, tag: &str) -> Option<&Node>

El primer hijo con esa etiqueta.

children_by<'a>(&'a self, tag: &'a str) -> impl Iterator<Item = &'a Node> + 'a

Los hijos con esa etiqueta.

first_child(&self) -> Option<&Node>

El primer hijo.

path(&self, tags: &[&str]) -> Option<&Node>

Baja por varias etiquetas.

content_bytes(&self) -> Option<&[u8]>

El contenido binario.

content_string(&self) -> Option<String>

El contenido como texto.

child_bytes(&self, tag: &str) -> Option<&[u8]>

El contenido binario de un hijo.

child_uint(&self, tag: &str) -> Option<u32>

El contenido de un hijo como número.

error_code(&self) -> Option<(u16, String)>

El código y el texto de un nodo de error.

as_str(&self) -> Option<&str>

El valor como texto.

to_jid(&self) -> Option<Jid>

El valor como JID.

encode(node: &Node) -> Vec<u8>

Codifica un nodo a bytes.

decode(data: &[u8]) -> Result<Node>

Decodifica bytes a un nodo.

decode_frame(data: &[u8]) -> Result<Node>

Decodifica un frame, descomprimiéndolo si hace falta.

JSON#

kind(&self) -> &'static str

El nombre del evento, como message o pollVote.

to_json(&self) -> Value

El evento en JSON, igual que en los demás lenguajes.

node_to_json(node: &Node) -> Value

Un nodo en JSON.

Credenciales#

generate() -> Self

Genera credenciales nuevas, sin vincular.

is_logged_in(&self) -> bool

Si tienen una cuenta vinculada.

generate(identity: &KeyPair, id: u32) -> Self

Genera y firma una signed pre-key.

as_str(self) -> &'static str

El nombre del tipo de dato.

Criptografía#

soblendwa::crypto expone las piezas que usa la librería. Normalmente no las necesitas.

sha256(data: &[u8]) -> [u8; 32]

SHA-256.

sha256_concat(parts: &[&[u8]]) -> [u8; 32]

SHA-256 de varias partes.

hmac_sha256(key: &[u8], parts: &[&[u8]]) -> [u8; 32]

HMAC-SHA256.

hmac_sha512(key: &[u8], parts: &[&[u8]]) -> [u8; 64]

HMAC-SHA512.

hkdf(ikm: &[u8], salt: Option<&[u8]>, info: &[u8], len: usize) -> Vec<u8>

HKDF-SHA256.

hkdf_array<const N: usize>(ikm: &[u8], salt: Option<&[u8]>, info: &[u8]) -> [u8; N]

HKDF-SHA256 hacia un arreglo.

gcm(key: &[u8]) -> Aes256Gcm

Un cifrador AES-256-GCM.

gcm_iv(counter: u32) -> [u8; 12]

El IV que usa Noise, a partir de un contador.

aes_gcm_encrypt(key: &[u8], iv: &[u8], aad: &[u8], plaintext: &[u8]) -> Result<Vec<u8>>

Cifra con AES-GCM.

aes_gcm_decrypt(key: &[u8], iv: &[u8], aad: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>>

Descifra con AES-GCM.

aes_cbc_encrypt(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>>

Cifra con AES-CBC.

aes_cbc_decrypt(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>>

Descifra con AES-CBC.

aes_ctr(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>>

Cifra o descifra con AES-CTR.

pbkdf2_sha256(password: &[u8], salt: &[u8], rounds: u32) -> [u8; 32]

PBKDF2 con SHA-256.

md5(data: &[u8]) -> [u8; 16]

MD5, solo porque WhatsApp lo pide en un dato de registro.

random_array<const N: usize>() -> [u8; N]

Bytes aleatorios en un arreglo.

random_vec(len: usize) -> Vec<u8>

Bytes aleatorios.

random_u32() -> u32

Un número aleatorio.

constant_time_eq(a: &[u8], b: &[u8]) -> bool

Compara en tiempo constante.

generate() -> Self

Un par de claves X25519 nuevo.

from_private(private: [u8; 32]) -> Self

Un par a partir de su clave privada.

shared_secret(&self, their_public: &[u8; 32]) -> [u8; 32]

El secreto compartido con otra clave pública.

signal_public(&self) -> [u8; 33]

La clave pública con el prefijo de Signal.

sign(&self, message: &[u8]) -> [u8; 64]

Firma con XEdDSA.

soblendwa::crypto::sign(private, message) -> [u8; 64]

Firma con XEdDSA a partir de una clave privada.

soblendwa::crypto::verify(public, message, signature) -> bool

Verifica una firma XEdDSA.

soblendwa::crypto::signal_key(public) -> [u8; 33]

Añade el prefijo de Signal a una clave pública.

soblendwa::crypto::strip_key_prefix(key) -> Result<[u8; 32]>

Quita el prefijo de Signal.

Utilidades de la raíz#

soblendwa::credits() -> String

Nombre, versión y autor.

soblendwa::NAME, VERSION, AUTHOR

Constantes con esos datos.

soblendwa::fetch_latest_version(http) -> Result<[u32; 3]>

Consulta la versión más reciente de WhatsApp Web.

soblendwa::DEFAULT_VERSION

La versión de respaldo.

soblendwa::COLLECTIONS

Las colecciones de app state.

soblendwa::guess_mimetype(path) -> Option<&str>

El tipo MIME según la extensión.

soblendwa::message::generate_message_id(me) -> String

Genera un id de mensaje.

soblendwa::json::media_json(message) -> Value

La multimedia de un mensaje en JSON.

soblendwa::imaging::image_dimensions(data) -> Option<(u32, u32)>

El ancho y el alto de una imagen.

soblendwa::imaging::thumbnail(data, width) -> Result<Vec<u8>>

Una miniatura JPEG.

soblendwa::imaging::profile_picture(data) -> Result<Vec<u8>>

La imagen recortada al centro y en 640×640.

Referencia de la API

Tipos

Los tipos públicos que devuelven y reciben los métodos, y sus campos.

Mensajes#

TipoCampos
MessageEventinfo, message, raw
MessageInfoid, chat, sender, sender_alt, sender_phone, sender_lid, from_me, is_group, push_name, timestamp, category, offline, edit, server_id, received_at
MessageKeyremote_jid, from_me, id, participant
SendOptionsid, edit, status_recipients, link_preview
PendingSendid, chat, queued_at, attempts
InteractiveReplyid, text, source, flow, params
ReplySourceButtons, List, Template, NativeFlow
NativeButtonQuickReply, Url, Copy, Call, Select
TemplateButtonQuickReply, Url, Call
Section, Rowtitle, rows; id, title, description

Multimedia#

TipoCampos o valores
MediaKindImage, Video, Audio, Document, Sticker, History, AppState, LinkThumbnail
MediaOptionsmimetype, caption, file_name, voice_note, gif, view_once, seconds, width, height, thumbnail
UploadedMediaurl, direct_path, media_key, file_sha256, file_enc_sha256, file_length, reused
DownloadableTrait con media_kind, media_key, direct_path, url, file_enc_sha256 y file_sha256
LinkPreviewurl, title, description, image_url, thumbnail, image, width, height, video
LinkPreviewConfigenabled, timeout, upload_hq_thumbnail, allow_private_hosts, max_html_bytes, max_image_bytes, user_agent, cache_ttl, fetcher
PreviewChoiceAuto, On, Off, Custom(Box<LinkPreview>)
LinkThumbnaildirect_path, media_key, file_sha256, file_enc_sha256, width, height

Grupos, canales y cuentas#

TipoCampos o valores
GroupInfojid, subject, subject_owner, subject_time, owner, creation, description, description_id, announce, locked, ephemeral, addressing_mode, join_approval, members_can_add, linked_parent, is_community, is_community_announce, participants
GroupParticipantjid, phone_number, lid, is_admin, is_super_admin
ParticipantActionAdd, Remove, Promote, Demote
ParticipantResultjid, status
JoinRequestjid, method, requested_at
LinkedGroupjid, subject, owner, created, size
NewsletterInfojid, name, description, invite, subscribers, verified, picture_path, muted, role, created, raw
NewsletterMessageserver_id, id, timestamp, views, message, reactions
OnWhatsAppquery, jid, exists, lid
Aboutjid, text, set_at
BotInfojid, persona_id
DisappearingModejid, duration, set_at
ReachoutTimelockactive, ends_at, enforcement
ChatPresenceComposing, Recording, Paused
PrivacySettingLastSeen, Online, ProfilePicture, About, ReadReceipts, GroupsAdd, Calls, Messages
PrivacyValueEveryone, Contacts, ContactsExcept, Nobody, MatchLastSeen, Known
ChatModificationArchive, Pin, Mute, MarkRead, Delete, Clear, Star, DeleteForMe, SaveContact, RemoveContact, Label, MessageLabel

Configuración#

TipoCampos o valores
Configdevice_name, platform, version, push_name, country_code, online_on_connect, auto_reconnect, connect_timeout, query_timeout, keepalive_interval, workers, ws_url, proxy, media_cache_ttl, send_limit, outbox, trust_root, media_endpoint, link_previews
PlatformChrome, Firefox, Safari, Edge, Opera, Desktop
SendLimitper_minute, burst, per_chat_per_minute, per_chat_burst
SendTimingsqueued, upload, prepare, encrypt, server
StatsVer Métricas
ProxyKindHttp, Socks5
EventStreamEl receptor de eventos: recv().await

Almacenamiento#

TipoCampos
Credsnoise_key, pairing_ephemeral_key, identity_key, signed_pre_key, registration_id, adv_secret_key, next_pre_key_id, first_unuploaded_pre_key_id, me, lid, push_name, account, platform, business_name, pairing_code, routing_info, registered, wa_version, app_state_key_id, signed_pre_key_rotated_at
BaileysImportme, sessions, pre_keys, sender_keys, app_state_keys, app_state_versions, lid_mappings, skipped, failed
ZapoImportme, sessions, pre_keys, sender_keys, app_state_keys, app_state_versions, lid_mappings, failed
ZapoExportsessions, pre_keys, sender_keys, app_state_keys, app_state_versions, failed

Los tipos de chats y de empresa están en Store de chats y en Catálogo de empresa.

Referencia de la API

Eventos

Todo lo que puede llegar por el canal de eventos, en Rust y en JSON.

En Node.js, Python y el servidor, cada evento es un objeto con un campo type, que es el nombre de esta tabla, y los demás campos en camelCase.

Conexión#

RusttypeCampos en JSONCuándo ocurre
Qr(String)qrcodeHay un QR nuevo para escanear
PairSuccess { .. }pairSuccessid, lid, platform, businessNameEl teléfono aprobó la vinculación
Connectedconnected—La sesión está lista
Disconnected { .. }disconnectedreason: { type, detail?, code? }, reconnectingSe perdió la conexión. Ver motivos
LoggedOut { reason }loggedOutreasonSe cerró la sesión y se borraron los datos
AccountBlocked { .. }accountBlockedcode, reason, expiresWhatsApp bloqueó la cuenta. No reintenta y conserva la sesión
OfflineSyncComplete { count }offlineSyncCompletecountYa llegaron los mensajes pendientes

Mensajes#

RusttypeCampos en JSONCuándo ocurre
Message(Box<MessageEvent>)messageVer abajoLlegó un mensaje descifrado
UndecryptableMessage { .. }undecryptableMessageinfo, errorNo se pudo descifrar y ya se pidió un reintento
MessageRecovered { .. }messageRecoveredid, chat, senderUno de esos llegó bien tras el reintento
Receipt(Receipt)receiptchat, sender, ids, receipt, timestamp, fromMeEntregado, leído, reproducido o reintento
ServerAck(ServerAck)serverAckid, chat, error, latencyMicros, timingsWhatsApp confirmó o rechazó un envío
PollVote(PollVote)pollVotechat, pollId, voter, selected, timestampAlguien votó
EventResponse(EventResponse)eventResponsechat, eventId, responder, response, extraGuests, timestampAlguien respondió a un evento
HistorySync(Box<HistorySync>)historySyncsyncType, progress, chunkOrder, conversations, pushNames, lidMappingsLlegó un bloque del historial

El mensaje en JSON#

{
  "type": "message",
  "info": {
    "id": "3EB0ABC123",
    "chat": "5215512345678@s.whatsapp.net",
    "sender": "5215512345678@s.whatsapp.net",
    "senderAlt": null,
    "senderPhone": "5215512345678@s.whatsapp.net",
    "senderLid": "100000000000001@lid",
    "fromMe": false,
    "isGroup": false,
    "pushName": "Ana",
    "timestamp": 1793729773,
    "category": null,
    "offline": false,
    "edit": null,
    "serverId": null,
    "elapsedMicros": 42
  },
  "kind": "text",
  "mediaType": null,
  "text": "hola",
  "media": null,
  "quoted": null,
  "viewOnce": false,
  "linkPreview": null,
  "ageSecs": 0,
  "quotedId": null,
  "mentions": [],
  "reaction": null,
  "reply": null,
  "proto": "CgRob2xh"
}

media.type puede ser image, video, gif, ptv, audio, ptt, document o sticker.

Tipos de recibo#

RustEn JSONQué significa
DelivereddeliveredLlegó al dispositivo
SendersenderLlegó a otro dispositivo tuyo
Read, ReadSelfread, read-selfLo leyeron. ReadSelf, tú desde otro dispositivo
Played, PlayedSelfplayed, played-selfLo reprodujeron
RetryretryNo lo pudieron descifrar y piden otro envío. Se atiende solo
InactiveinactiveEl dispositivo estaba inactivo
PeerMessagepeer_msgLo recibió otro dispositivo tuyo
HistorySynchist_syncRecibo de historial
ServerErrorserver-errorEl servidor no pudo entregarlo
Other(texto)ese textoOtro tipo

Personas, grupos y chats#

RusttypeCampos en JSONCuándo ocurre
Presence(Presence)presencefrom, available, lastSeenAlguien se conectó o desconectó
ChatState(ChatState)chatStatechat, sender, stateAlguien escribe o graba
GroupUpdate(GroupUpdate)groupUpdategroup, author, action, participants, timestampCambió un grupo
ChatUpdate { .. }chatUpdateupdate, timestamp, initialSyncCambió un chat o contacto
PictureChanged { .. }pictureChangedjid, author, pictureId, removed, timestampAlguien cambió o quitó su foto Experimental
AboutChanged { .. }aboutChangedjid, about, timestampAlguien cambió su información Experimental
BlocklistChanged { items }blocklistChangeditems: [{ jid, phone, blocked }]Cambió tu lista de bloqueados Experimental
PrivacyChanged { settings }privacyChangedsettings: [{ name, value }]Cambió un ajuste de privacidad Experimental
Call(CallEvent)callid, from, call, video, isGroup, timestampLlamada entrante, aceptada, rechazada o terminada

Avanzado#

RusttypeCampos en JSONCuándo ocurre
Notification(Node)notificationnode: { tag, attrs, content }Una notificación sin tipo propio
Unhandled(Node)unhandlednode: { tag, attrs, content }Un nodo que la librería no atiende

Referencia de la API

Errores

Los errores que puede devolver cualquier método, y qué hacer con cada uno.

soblendwa::Result<T> usa soblendwa::Error. En los demás lenguajes llegan como una excepción con este mismo texto.

ErrorTextoQué significaQué hacer
WebSocketwebsocket error: …Falló el socketRevisa la red. Se reintenta solo
Ioio error: …Falló el disco o la redRevisa la ruta y los permisos
Httphttp error: …Falló una petición HTTPRevisa la red o el proxy
Protoprotobuf decode error: …Un protobuf no se pudo leerAbre un issue si se repite
Jsonjson error: …Un JSON no se pudo leerRevisa los parámetros
Binarymalformed binary node: …Un nodo está mal formadoAbre un issue si se repite
Cryptocrypto failure: …Falló una operación de cifradoPuede ser un archivo dañado
Signalsignal protocol: …Falló el protocolo SignalNormalmente se recupera con un reintento
Handshakenoise handshake failed: …Falló el handshake con el servidorRevisa la red, el proxy o trust_root
Server { code, text }server answered with error …WhatsApp rechazó la peticiónEl código indica el motivo
Timeoutthe request timed outNo hubo respuesta a tiempoReintenta. error.is_timeout() lo detecta
NotConnectedthe client is not connectedNo hay conexiónEspera a Connected
NotLoggedInthe client is not logged inLa cuenta no está vinculadaVincúlala
InvalidJidinvalid jid: …El JID no es válidoRevisa el texto
Mediamedia error: …Falló la subida o descargaRevisa el archivo
Storestore error: …Falló el almacenamientoRevisa tu base de datos
MissingAppStateKeymissing app state key: …Falta una clave de app stateSe pide sola al teléfono
RateLimited { chat, retry_after }send limit reached for …Pasaste el límiteEspera retry_after
AccountBlockedthe account is blocked by whatsapp: …Cuenta bloqueadaRevísala en el teléfono
OtherVariosOtro errorLee el mensaje
is_timeout(&self) -> bool

Indica si el error es Timeout.

Proyecto

Cómo funciona

Qué ocurre por dentro desde que abres la conexión hasta que sale un mensaje.

Conectar#

  1. WebSocket. Se abre un WSS hacia WhatsApp, a través del proxy si lo configuraste, con TCP_NODELAY.
  2. 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.
  3. Inicio de sesión. Si no hay cuenta vinculada, se registra y llegan los QR. Si la hay, se presenta con sus claves.
  4. 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.
  5. Listo. Llega Event::Connected, y empiezan el keepalive y el vaciado del outbox.

Recibir un mensaje#

  1. Llega un frame cifrado por el WebSocket y se descifra con AES-GCM.
  2. El decodificador lo convierte en un nodo binario, con límites contra entradas hostiles.
  3. Pasa a la cola de su chat, así los chats distintos van en paralelo y cada uno conserva su orden.
  4. Se descifra con Signal: Double Ratchet en privado, Sender Keys en grupos. Si falla, se pide un reintento.
  5. Se descarta si es un duplicado, se resuelven el número y el LID, y se desenvuelve el protobuf.
  6. Se envía el recibo a WhatsApp, y Event::Message llega a tu bot.

Enviar un mensaje#

  1. Límite. Se comprueba el límite de envíos, del total y del chat.
  2. Outbox. El mensaje se guarda, por si se cae la conexión.
  3. Cola. Entra en la cola de su chat, para conservar el orden.
  4. Vista previa. Si trae un enlace, se genera. Nunca frena el envío más de lo configurado.
  5. 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.
  6. Cifrado. Se cifra para cada dispositivo. En grupos, se usa la sender key.
  7. Confirmación. WhatsApp responde, llega Event::ServerAck con sus tiempos, y el mensaje sale del outbox.

Cómo está organizado el código#

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

Seguridad del código#

  • #![forbid(unsafe_code)] en el núcleo.
  • El decodificador binario resiste entradas aleatorias, anidamiento hostil y bombas de compresión, sin pánicos ni desbordamientos.
  • La multimedia descargada se verifica con HMAC y SHA-256 antes de entregarse.
  • El protocolo Signal está probado contra libsignal-node en las dos direcciones.

Proyecto

Comparación

Cómo se compara con otras librerías, incluido lo que ellas tienen y SoblendWA no.

SoblendWABaileys 7whatsmeowzapo-js 1.11
LenguajeRustJavaScriptGoTypeScript
Se usa desdeRust, Node.js, Python y cualquier lenguaje por WebSocket, HTTP o webhooksJavaScript, TypeScriptGoJavaScript, TypeScript
Almacenamiento incluidoArchivos, memoria, SQLite, PostgreSQL, Redis y MongoDBArchivosSQLite y PostgreSQLSQLite, PostgreSQL, MySQL, Redis y MongoDB, en paquetes aparte
Vista previa automática de enlacesSí, con bloqueo de hosts privadosCon link-preview-jsA manoSí
Outbox persistente y límite de envíosSí———
Importar la sesión de otra libreríaBaileys y zapo, con rollback a zapo———
WhatsApp falso para pruebasIncluido——@zapo-js/fake-server
Llamadas con audio———@zapo-js/voip
Actuar como teléfono principal———Sí
MadurezVersión 0.1, faltan pruebas largasAños de usoAños de usoEstable desde la 1.0

— significa que no viene incluido o que no lo encontramos en esa versión. Si algo cambió o nos equivocamos, abre un issue y lo corregimos.

Los números medidos están en Rendimiento. Para pasar tu bot, ve a Migrar.

Cuál elegir#

  • Elige SoblendWA si te importan la velocidad y la memoria, quieres varias cuentas en un servidor pequeño, o tu bot no está en JavaScript ni en Go.
  • Elige Baileys o whatsmeow si necesitas lo más probado en producción, con años de uso y una comunidad grande.
  • Elige zapo si necesitas llamadas con audio o actuar como teléfono principal.

Proyecto

Estado del proyecto

Versión 0.1. Lo que se probó de verdad y contra qué, y lo que aún no.

Probado en vivo con un teléfono real Probado#

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.

Probado contra otras librerías Probado#

  • 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.

Probado contra el WhatsApp falso Probado#

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.

Probado en CI#

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.

Experimental Experimental#

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.

No incluido No incluido#

A diferencia de zapo, SoblendWA no incluye:

  • Actuar como teléfono principal: registro móvil y vincular otros dispositivos.
  • Mensajes de Meta AI con su cifrado propio (msmsg).
  • Confirmación por passkey al vincular.
  • Llamadas con audio y video. Solo recibe los eventos, las rechaza y crea enlaces.
  • Telemetría WAM.
  • Un store de MySQL incluido, aunque puedes escribirlo tú.

Proyecto

Aviso y licencia

Lo que debes saber antes de usar SoblendWA con una cuenta real.

Aviso#

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.

Licencia#

MIT © 2026 Soblend Development Studio. El texto completo está en LICENSE.

Agradecimientos#

SoblendWA es una implementación propia, y existe gracias a lo que documentaron y publicaron Baileys, whatsmeow, libsignal y zapo.

Dónde conseguir ayuda#

SoblendWA 0.1.0 · MIT · Hecho por Soblend Development Studio. Proyecto independiente, sin relación con WhatsApp ni Meta.