Perfil y privacidad
Para qué sirve
Sección titulada «Para qué sirve»Con estas rutas tu app maneja lo que el número vinculado muestra de sí mismo y a quién:
- el perfil propio: el nombre visible, la Info (el recado,
recadoen la API) y la foto; - la privacidad: quién ve la última vez, la foto o los estados, quién te agrega a grupos, las confirmaciones de lectura y el resto de opciones de WhatsApp;
- el temporizador por defecto de los mensajes temporales en los chats nuevos;
- los bloqueos: la lista de contactos bloqueados, bloquear y desbloquear.
Los cambios son comandos durables: el motor responde 202 y los ejecuta contra WhatsApp. Lo
que pide el motor queda con confirmado: false hasta que WhatsApp dice lo mismo (un evento o la
consulta que el motor hace después del cambio).
El perfil de otros (su nombre, su Info, su foto) está en Contactos.
Requisitos
Sección titulada «Requisitos»- Una instancia
conectado. Las consultas conactualizar=trueresponden 409no_conectadasin conexión; un comando sin conexión vuelve a la cola y se reintenta. - Una API key con estos scopes:
| Operación | Ruta | Scope |
|---|---|---|
| Ver el perfil propio | GET /v1/instances/{id}/profile |
profile:read y/o privacy:read |
| Ver la privacidad | GET /v1/instances/{id}/privacy |
privacy:read |
| Ver los bloqueados | GET /v1/instances/{id}/blocklist |
privacy:read |
| Cambiar el nombre | PUT /v1/instances/{id}/profile/name |
profile:write |
| Cambiar la Info | PUT /v1/instances/{id}/profile/about |
profile:write |
| Cambiar o quitar la foto | PUT o DELETE /v1/instances/{id}/profile/picture |
profile:write |
| Cambiar una opción de privacidad | PUT /v1/instances/{id}/privacy |
privacy:write |
| El temporizador por defecto | PUT /v1/instances/{id}/privacy/disappearing |
privacy:write |
| Bloquear o desbloquear | POST /v1/instances/{id}/blocklist |
privacy:write |
GET …/profile muestra a cada key solo lo suyo: con profile:read, el nombre, la Info y la foto;
con privacy:read, la privacidad, el temporizador y cuántos bloqueados hay. Sin ninguno de los
dos, 403.
Los comandos piden una Idempotency-Key (de 1 a 200 caracteres). Para cambiar la foto, súbela
antes con media:write (ver Multimedia).
Los ejemplos de Go y JavaScript usan una función auxiliar, llamar, que se define en el primer
paso: el SDK de Go no trae métodos de perfil, así que es HTTP directo con el token del cliente.
1. Consulta el perfil propio
Sección titulada «1. Consulta el perfil propio»GET /v1/instances/{id}/profile responde con lo que el motor sabe. Cada dato del perfil trae
valor, desde (quién lo fijó: whatsapp si fue el teléfono, motor si fue un comando, o
consulta), confirmado y momento. La foto se ve por su ID (vacío: sin foto). Con
actualizar=true el motor consulta antes al servidor lo que la key puede leer.
curl "https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/profile?actualizar=true" \ -H "Authorization: Bearer TU_API_KEY"import motor "github.com/projectx27/motor-whatsapp/sdk/go"
// llamar: el SDK no trae el perfil, así que es HTTP directo con el token del cliente.// clave va como Idempotency-Key (vacía en las consultas).func llamar(ctx context.Context, c *motor.Cliente, metodo, ruta, clave string, cuerpo, destino any) error { var r io.Reader if cuerpo != nil { b, err := json.Marshal(cuerpo) if err != nil { return err } r = bytes.NewReader(b) } req, err := http.NewRequestWithContext(ctx, metodo, c.Base+ruta, r) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.Token) if cuerpo != nil { req.Header.Set("Content-Type", "application/json") } if clave != "" { req.Header.Set("Idempotency-Key", clave) } res, err := c.HTTP.Do(req) if err != nil { return err } defer res.Body.Close() if res.StatusCode >= 300 { return fmt.Errorf("motor: %d", res.StatusCode) } if destino == nil { return nil } return json.NewDecoder(res.Body).Decode(destino)}
type dato struct { Valor string `json:"valor"` Desde string `json:"desde"` Confirmado bool `json:"confirmado"`}
// En tu código:c := motor.Nuevo("https://motor.ejemplo.com", apiKey)var perfil struct { Nombre *dato `json:"nombre"` Recado *dato `json:"recado"` Foto *dato `json:"foto"` Privacidad map[string]string `json:"privacidad"`}err := llamar(ctx, c, http.MethodGet, "/v1/instances/"+instanciaID+"/profile?actualizar=true", "", nil, &perfil)const base = 'https://motor.ejemplo.com';
// llamar: una petición a /v1 con el token; clave va como Idempotency-Key.async function llamar(metodo, ruta, { clave, cuerpo } = {}) { const headers = { Authorization: `Bearer ${apiKey}` }; if (cuerpo !== undefined) headers['Content-Type'] = 'application/json'; if (clave) headers['Idempotency-Key'] = clave; const res = await fetch(base + ruta, { method: metodo, headers, body: cuerpo === undefined ? undefined : JSON.stringify(cuerpo), }); const datos = await res.json().catch(() => null); if (!res.ok) throw new Error(`${res.status} ${datos?.type ?? ''} ${datos?.detail ?? ''}`); return datos;}
const perfil = await llamar('GET', `/v1/instances/${instanciaId}/profile?actualizar=true`);{ "nombre": { "valor": "Sucursal centro", "desde": "motor", "confirmado": true, "momento": "2026-10-24T15:25:00Z" }, "recado": { "valor": "Abierto de 9 a 18 h", "desde": "consulta", "confirmado": true, "momento": "2026-10-24T15:26:00Z" }, "foto": { "valor": "FOTO_ID", "desde": "motor", "confirmado": true, "momento": "2026-10-24T15:27:00Z" }, "privacidad": { "groupadd": "contacts", "last": "contacts", "status": "contacts", "profile": "all", "readreceipts": "all", "online": "all", "calladd": "all", "messages": "all", "defense": "off", "stickers": "contacts" }, "temporizador_por_defecto_seg": 0, "bloqueados": 3, "consultado_en": "2026-10-24T15:26:00Z"}GET …/privacy devuelve solo la parte de privacidad (con privacy:read).
2. Cambia el nombre, la Info o la foto
Sección titulada «2. Cambia el nombre, la Info o la foto»| Cambio | Ruta | Cuerpo | Tipo de comando |
|---|---|---|---|
| Nombre visible | PUT …/profile/name |
{"nombre": "…"}, de 1 a 25 caracteres |
profile.name |
| Info | PUT …/profile/about |
{"texto": "…"}; vacío la deja en blanco |
profile.about |
| Foto | PUT …/profile/picture |
{"media_id": "…"} |
profile.picture |
| Quitar la foto | DELETE …/profile/picture |
sin cuerpo | profile.picture |
- El nombre sale por la sincronización de la app de WhatsApp; su eco lo confirma.
- La Info que muestran los teléfonos es el text status de WhatsApp. El motor la escribe como
WhatsApp Web, con la forma que elige el operador en Ajustes (
perfil.forma_info, fijada al aceptar el comando). Con la forma de fábrica,24h, la Info se ve 24 horas y después deja de verse, y admite hasta 50 caracteres. Elresultdel comando dice por qué vía salió (via), con qué duración (duracion_seg) y qué pasó con el campo viejo (clasico:okofallo). - La foto es experimental: un JPEG subido antes a esta instancia, ya
listo, de hasta 2 MB y de 192 a 2048 píxeles por lado. WhatsApp solo acepta 640 × 640, así que el motor la recorta al centro y la deja en esa medida.
curl -X PUT https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/profile/name \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: perfil-nombre-centro" \ -d '{"nombre": "Sucursal centro"}'
curl -X PUT https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/profile/about \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: perfil-info-horario" \ -d '{"texto": "Abierto de 9 a 18 h"}'
curl -X PUT https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/profile/picture \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: perfil-foto-logo" \ -d '{"media_id": "8a1d2c3e-0000-4000-8000-0000000000e3"}'// llamar: la función auxiliar del paso 1.ruta := "/v1/instances/" + instanciaID + "/profile"
var cmd motor.Comandoerr := llamar(ctx, c, http.MethodPut, ruta+"/name", "perfil-nombre-centro", map[string]string{"nombre": "Sucursal centro"}, &cmd)
err = llamar(ctx, c, http.MethodPut, ruta+"/about", "perfil-info-horario", map[string]string{"texto": "Abierto de 9 a 18 h"}, &cmd)
err = llamar(ctx, c, http.MethodPut, ruta+"/picture", "perfil-foto-logo", map[string]string{"media_id": mediaID}, &cmd)const ruta = `/v1/instances/${instanciaId}/profile`;
await llamar('PUT', `${ruta}/name`, { clave: 'perfil-nombre-centro', cuerpo: { nombre: 'Sucursal centro' },});
await llamar('PUT', `${ruta}/about`, { clave: 'perfil-info-horario', cuerpo: { texto: 'Abierto de 9 a 18 h' },});
await llamar('PUT', `${ruta}/picture`, { clave: 'perfil-foto-logo', cuerpo: { media_id: mediaId },});{ "via": "mex", "duracion_seg": 86400, "clasico": "ok" }3. Cambia la privacidad y el temporizador por defecto
Sección titulada «3. Cambia la privacidad y el temporizador por defecto»PUT …/privacy cambia una opción por comando (privacy.set). El servidor devuelve la
privacidad completa, que queda confirmada, y llega un privacy.changed.
opcion |
Qué controla | Valores |
|---|---|---|
groupadd |
Quién te agrega a grupos | all, contacts, contact_blacklist, none |
last |
Hora de última vez | all, contacts, contact_blacklist, none |
status |
Quién ve tus estados | all, contacts, contact_blacklist, none |
profile |
Quién ve tu foto | all, contacts, contact_blacklist, none |
readreceipts |
Confirmaciones de lectura | all, none |
online |
Quién ve que estás en línea | all, match_last_seen |
calladd |
Quién te llama | all, known |
messages |
Quién te escribe | all, contacts |
defense |
Protección avanzada | on_standard, off |
stickers |
Stickers | contacts, contact_allowlist, none |
Las listas de contact_blacklist y contact_allowlist se editan en el teléfono. Un valor que la
opción no acepta es 422.
PUT …/privacy/disappearing fija el temporizador de los chats nuevos (privacy.disappearing)
con segundos: 0 (desactivado), 86400 (24 horas), 604800 (7 días) o 7776000 (90 días). Cada
chat sigue con el suyo (eso es el mensaje disappearing, ver Mensajes). El
SDK no puede leer este valor: el motor muestra el último que fijó o que avisó WhatsApp.
curl -X PUT https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/privacy \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: privacidad-grupos-contactos" \ -d '{"opcion": "groupadd", "valor": "contacts"}'
curl -X PUT https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/privacy/disappearing \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: temporizador-7-dias" \ -d '{"segundos": 604800}'// llamar: la función auxiliar del paso 1.ruta := "/v1/instances/" + instanciaID + "/privacy"
var cmd motor.Comandoerr := llamar(ctx, c, http.MethodPut, ruta, "privacidad-grupos-contactos", map[string]string{"opcion": "groupadd", "valor": "contacts"}, &cmd)
err = llamar(ctx, c, http.MethodPut, ruta+"/disappearing", "temporizador-7-dias", map[string]int{"segundos": 604800}, &cmd)const ruta = `/v1/instances/${instanciaId}/privacy`;
await llamar('PUT', ruta, { clave: 'privacidad-grupos-contactos', cuerpo: { opcion: 'groupadd', valor: 'contacts' },});
await llamar('PUT', `${ruta}/disappearing`, { clave: 'temporizador-7-dias', cuerpo: { segundos: 604800 },});4. Bloquea y desbloquea
Sección titulada «4. Bloquea y desbloquea»GET …/blocklist devuelve los bloqueados según lo último que dijo WhatsApp: cada uno con su
contacto (peer), su telefono cuando se conoce, bloqueado, cambio_en y desde. Con
actualizar=true el motor pide antes la lista al servidor.
POST …/blocklist bloquea o desbloquea un destino (telefono, jid o peer_ref) con
bloquear: true o false (contact.block). El servidor devuelve la lista completa, que queda
confirmada, y llega un blocklist.changed con el command_id. Un contacto del que el motor no
conoce su LID puede fallar con sin_identidad.
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/blocklist \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: bloquear-spam-0042" \ -d '{"destino": {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b4"}, "bloquear": true}'
curl "https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/blocklist?actualizar=true" \ -H "Authorization: Bearer TU_API_KEY"// llamar: la función auxiliar del paso 1.ruta := "/v1/instances/" + instanciaID + "/blocklist"
var cmd motor.Comandoerr := llamar(ctx, c, http.MethodPost, ruta, "bloquear-spam-0042", map[string]any{ "destino": map[string]string{"peer_ref": contactoRef}, "bloquear": true,}, &cmd)
var lista struct { Bloqueados []struct { Contacto struct{ PeerRef string `json:"peer_ref"` } `json:"contacto"` Bloqueado bool `json:"bloqueado"` } `json:"bloqueados"`}err = llamar(ctx, c, http.MethodGet, ruta+"?actualizar=true", "", nil, &lista)const ruta = `/v1/instances/${instanciaId}/blocklist`;
await llamar('POST', ruta, { clave: 'bloquear-spam-0042', cuerpo: { destino: { peer_ref: contactoRef }, bloquear: true },});
const { bloqueados } = await llamar('GET', `${ruta}?actualizar=true`);El comando y sus eventos
Sección titulada «El comando y sus eventos»Los comandos (profile.name, profile.about, profile.picture, privacy.set,
privacy.disappearing y contact.block) nacen aceptado y terminan completado o fallido. El
efecto llega con su command_id y desde: motor; un cambio hecho en el teléfono, con
desde: whatsapp y sin command_id.
profile.updated
Sección titulada «profile.updated»Cambió el perfil propio. campo es nombre, recado o foto; valor es el nombre, la Info o el
ID de la foto, y una foto quitada trae quitada: true. desde es whatsapp, motor o
consulta. Está también en el Catálogo de eventos.
{ "campo": "recado", "valor": "Abierto de 9 a 18 h", "desde": "motor", "momento": "2026-10-24T15:26:00Z" }privacy.changed
Sección titulada «privacy.changed»Cambió la privacidad. privacidad trae todas las opciones con su valor nuevo y cambiadas, solo
las que cambiaron. Un cambio del temporizador por defecto trae
cambiadas: ["temporizador_por_defecto"] y temporizador_por_defecto_seg.
{ "privacidad": { "groupadd": "contacts", "last": "contacts", "status": "contacts", "profile": "all", "readreceipts": "all", "online": "all", "calladd": "all", "messages": "all", "defense": "off", "stickers": "contacts" }, "cambiadas": ["groupadd"], "desde": "motor"}blocklist.changed
Sección titulada «blocklist.changed»Cambiaron los bloqueos. cambios trae cada contacto que cambió, con su estado nuevo.
{ "cambios": [ { "contacto": { "peer_ref": "…", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] }, "telefono": "+NUMERO_DEL_CONTACTO", "bloqueado": true, "cambio_en": "2026-10-24T15:40:00Z", "desde": "motor" } ], "desde": "motor"}Errores comunes
Sección titulada «Errores comunes»| HTTP | error_code |
Cuándo |
|---|---|---|
| 403 | — | A tu key le falta el scope (profile:read ni privacy:read en GET …/profile; profile:write, privacy:read o privacy:write en las demás). |
| 404 | — | El media_id de la foto no existe en esta instancia. |
| 409 | no_conectada |
Una consulta con actualizar=true sin conexión. |
| 409 | — | La misma Idempotency-Key con otros datos. |
| 422 | — | Un nombre fuera de 1 a 25 caracteres; una Info de más de 50 (139 solo con la forma clasico); un valor que la opción de privacidad no acepta; un temporizador que no es 0, 86400, 604800 ni 7776000. |
| 422 | foto_invalida |
La foto no es un JPEG listo de esta instancia, pesa más de 2 MB o no mide de 192 a 2048 píxeles por lado. |
| 503 | multimedia_deshabilitada |
Cambiar la foto sin almacenamiento de multimedia configurado. |
Los error_code de un comando que falla:
error_code |
Qué pasó |
|---|---|
foto_invalida |
El servidor rechazó la imagen. |
medio_no_disponible |
La foto ya no existe o no se pudo leer: no salió nada. |
sin_identidad |
Bloquear un contacto del que el motor no conoce su LID. |
rechazado_por_whatsapp |
WhatsApp rechazó el cambio; server_code trae su código. |
operacion_fallida |
El cambio no se pudo hacer; el motor lo reintenta antes de darlo por fallido. |
Límites de WhatsApp
Sección titulada «Límites de WhatsApp»- La Info cambiada desde el teléfono no avisa a la propia instancia (falla F34, caso A45,
22/10/2026, abierta y de gravedad baja): la instancia no recibe
profile.updated, aunque las otras instancias que tienen el número como contacto sí recibencontact.about. Un cambio de nombre sí llega. Consulta el perfil conGET …/profile?actualizar=truecuando necesites el valor vigente. Ver Info propia cambiada desde el teléfono. - Borrar la Info deja el campo viejo (caso A45, 21/10/2026): con
texto: ""la Info queda en blanco (via: mex,duracion_seg: 0), pero el campo viejo del servidor conserva el texto anterior (clasico: fallo) y una consulta posterior puede devolverlo. - La foto solo en 640 × 640 (caso A46, falla F26, corregida el 21/10/2026): el servidor
rechazaba con
foto_invalidacualquier otra medida; desde la corrección el motor la deja en 640 × 640 antes de mandarla. Ver Otros límites observados. - Privacidad y temporizador (casos A47 y A50, 20 y 21/10/2026): cada cambio desde el motor y
desde el teléfono llegó como
privacy.changedcon solo la opción que cambió; el temporizador por defecto cambiado en el teléfono llegó contemporizador_por_defecto_seg(21/10, con la corrección R10). La opción «Stickers» no existe en el cliente normal de Android probado: solo se pudo cambiar desde el motor. - Bloqueos (caso A48, falla F28, corregida el 21/10/2026): bloquear y desbloquear desde el
motor llega como
blocklist.changedcondesde: motory sucommand_id; desde el teléfono, condesde: whatsapp. Mientras está bloqueado, el contacto no puede escribirle al número.