1. Inicio
  2. API
API · REST v1 · JSON sobre HTTPS

La API de outreach por DM en TikTok

TikTok no tiene una API oficial para enviar cold DMs: su Business Messaging API solo te deja responder a quien te escribe primero. La API REST de Hooka es la capa programática gestionada para el outreach saliente por DM en TikTok: conectas tus cuentas de TikTok por QR, creas campañas con variantes de mensaje y follow-ups, añades hasta 1.000 destinatarios por llamada y recibes los envíos y las respuestas por webhooks firmados o consultando un feed de eventos.

Última actualización: 30 sept 2026 · Por el equipo de Hooka

¿Existe una API oficial de TikTok para enviar DMs?

No. A junio de 2026, TikTok no ofrece ninguna API oficial para iniciar mensajes directos a personas que no te han escrito antes. Su única superficie oficial de mensajería, la Business Messaging API, está pensada para la atención al cliente entrante, no para el outreach.

Sus restricciones lo dejan claro: el usuario tiene que escribir primero a tu negocio, tienes una ventana de respuesta de 48 horas, puedes enviar unos 10 mensajes automatizados por ventana y el envío masivo (broadcasting) está prohibido. Por eso las herramientas entrantes construidas sobre ella, con ManyChat como la más conocida, no pueden iniciar ni un solo cold DM.

Así que toda herramienta real de cold DM funciona fuera de la API oficial, Hooka incluida. Lo decimos claro porque condiciona todo el diseño: la API de Hooka es una capa programática gestionada sobre cuentas reales de TikTok. Tú llamas a endpoints REST limpios; Hooka maneja navegadores reales con la sesión de cada cuenta, conectados a través de una IP residencial dedicada del país de tus cuentas, aplica en el servidor el espaciado entre mensajes y los topes diarios por cuenta y, cuando TikTok avisa de que vas demasiado rápido, devuelve el mensaje a la cola y pausa esa cuenta durante 5 minutos. Las cifras que hay detrás de esos valores están en nuestra guía de límites de DM en TikTok en 2026.

300
peticiones / minuto por workspace
1.000
targets por llamada
12
tipos de evento webhook
24 h
de ventana de idempotencia

¿Qué necesitas antes de la primera llamada?

  1. Una suscripción activa (o en periodo de prueba). Tu plan fija cuántas cuentas de TikTok puedes conectar (seats). Sin suscripción, POST /v1/senders devuelve 403 no_active_subscription y el motor no envía nada, aunque una campaña figure como running y la API no dé ningún error.
  2. El país de cada cuenta. Cada cuenta que conectas recibe su propia IP residencial dedicada de su país, para que TikTok no vea un inicio de sesión desde un centro de datos. Pasa country (ISO 3166-1 alfa-2) al crear el sender o deja que use el país por defecto de tu workspace. Después no se puede cambiar.
  3. Una API key hk_live_. Se crea en la página Developers de tu panel. Se guarda solo como hash sha256 y se muestra una única vez, así que cópiala al crearla.

Una sola llamada te dice si estás listo: seats.used tiene que ser menor que seats.limit, y exit_ip.region es el país por defecto que reciben los senders nuevos cuando no pasas country.

export HOOKA_API_KEY="hk_live_..."   # tu clave, de la página Developers

curl https://api.hooka.to/v1/workspace \
  -H "Authorization: Bearer $HOOKA_API_KEY"

# → 200
{
  "id": "ws_3f1c2a9e-7b4d-4c1e-9a0b-5d6e7f8a9b0c",
  "object": "workspace",
  "name": "Default",
  "created": 1788000000,
  "exit_ip": { "status": "ready", "region": "ES", "suggested_region": null, "per_sender": true },
  "seats": { "limit": 3, "used": 1 }
}

"no_region" solo significa que no hay país por defecto confirmado: no bloquea, pasa country al crear el sender. Si falta algo, POST /v1/senders responde 400 country_required, 403 no_active_subscription o 403 sender_limit_reached, y 409 exit_ip_not_ready (con retry_after) si ahora mismo no hay IP disponible en ese país ni en los más cercanos.

¿Cómo funciona la API de Hooka?

Si has usado Stripe, te resultará familiar: REST sobre HTTPS, JSON, claves de tipo bearer, IDs con prefijo, paginación por cursor e idempotencia. Las convenciones:

  • URL base: todos los endpoints cuelgan de https://api.hooka.to/v1. Está pensada para llamadas de servidor a servidor: no pongas tu clave en código que se ejecuta en el navegador.
  • Claves: envía Authorization: Bearer hk_live_.... Existen claves hk_test_ (POST /v1/api_keys con {"mode": "test"}), pero son de solo lectura: cualquier POST, PATCH o DELETE devuelve 403 test_key_read_only. No hay sandbox: toda escritura actúa sobre tus cuentas reales de TikTok.
  • Solo JSON: envía siempre los cuerpos con Content-Type: application/json; si no, recibes 415 unsupported_content_type (ojo: curl -d sin esa cabecera envía un formulario). El cuerpo tiene un máximo de 1 MB.
  • IDs con prefijo: cada ID dice a qué apunta: snd_ senders (cuentas de TikTok), cmp_ campañas, tgt_ targets, con_ conversaciones, msg_ mensajes, rpl_ respuestas encoladas (las devuelve POST /v1/conversations/{id}/messages; ningún endpoint las consulta), sup_ supresiones, we_ endpoints de webhook, evt_ eventos, key_ API keys y ws_ tu workspace.
  • Fechas: enteros Unix en segundos, en UTC (created, sent_at…).
  • Paginación: las listas devuelven {"object": "list", "data": [...], "has_more": true, "next_cursor": "cmp_..."}. Pasa limit (de 1 a 100; 50 por defecto) y, mientras has_more sea true, pide la página siguiente con starting_after=<next_cursor>. Dos excepciones: en GET /v1/events?after=... se sigue con after=<next_cursor>, nunca con starting_after (junto a after da un 400 y, por sí solo, devuelve los eventos anteriores al cursor, del más reciente al más antiguo); y GET /v1/webhook_endpoints y GET /v1/api_keys no se paginan: lo devuelven todo en {"object": "list", "data": [...]}, sin has_more ni next_cursor, e ignoran limit.
  • Idempotencia: todos los POST salvo POST /v1/api_keys aceptan la cabecera Idempotency-Key (de 1 a 255 caracteres). Las claves se guardan 24 horas por workspace. Si repites la petición con la misma clave, recibes la respuesta guardada con Idempotent-Replayed: true, sin que la acción se ejecute dos veces. La misma clave con otra petición da 409 idempotency_key_reuse; si la original sigue en curso, recibes 409 idempotency_key_in_use: reintenta en unos segundos con la misma clave. Los 5xx, los 429 y los errores que dependen del estado del momento (como no_sender_available) no se guardan, así que un reintento con la misma clave se vuelve a ejecutar. Una respuesta repetida es una foto de la original, no el estado actual.
  • Límite de peticiones: 300 por minuto por workspace, compartidas entre todas tus claves y tokens OAuth. Cada respuesta lleva x-ratelimit-limit, x-ratelimit-remaining y x-ratelimit-reset; si te pasas, recibes 429 rate_limited con la cabecera Retry-After y retry_after en el cuerpo.
  • Trazabilidad: cada respuesta lleva una cabecera Hooka-Request-Id, que se repite como error.request_id en los errores. Cítala si escribes a soporte.

Errores

Todos los errores usan el mismo sobre. code es el valor estable con el que decidir qué hacer, y param señala el campo culpable cuando lo hay:

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "Number must be greater than or equal to 30",
    "param": "settings.rate_limit_seconds",
    "request_id": "req_9eab538a53ad4f74840f1ee1927578d3"
  }
}

El type casi siempre fija el código HTTP: invalid_request_error 400, authentication_error 401, permission_error 403, not_found_error 404, conflict_error 409, rate_limit_error 429 y api_error 500. Las dos excepciones: invalid_request_error también llega con 413 (body_too_large) y 415 (unsupported_content_type), y api_error puede ser cualquier 5xx. Un recurso de otro workspace se comporta exactamente igual que uno que no existe (404).

Casi todos los endpoints ignoran en silencio los campos del cuerpo que no conocen, así que revisa bien los nombres: un ajuste inventado como spacing_seconds simplemente se descarta (el espaciado entre DMs es settings.rate_limit_seconds). La excepción es PATCH /v1/senders/{id}, que solo acepta label y rechaza cualquier otro campo con un 400 cuyo param es ese campo. Un PATCH /v1/webhook_endpoints/{id} sin ninguno de url, enabled_events o status también es un 400.

Las campañas que crees por API y las que crees en el panel usan el mismo motor de envío y se ven en ambos sitios: todo el conjunto de funciones, del reparto entre cuentas al inbox unificado, trabaja sobre los mismos datos que tus llamadas.

Quickstart: de la API key a una campaña en marcha

Este es el flujo completo en cuatro pasos: conecta una cuenta, crea una campaña, añade destinatarios y arráncala. Después, escucha los eventos. Los ejemplos usan la variable HOOKA_API_KEY del apartado anterior; sustituye los IDs por los que te devuelva tu API.

1. Conecta una cuenta de TikTok (sender)

Un sender es una de tus cuentas de TikTok. La conexión es solo por QR: Hooka nunca te pide la contraseña de TikTok. POST /v1/senders crea el sender, con su propia IP residencial dedicada del country de la cuenta, y abre un intento de conexión, pero la respuesta no trae el QR: lo genera un navegador en cuanto la IP está lista, normalmente en segundos (mientras tanto exit_ip.status es provisioning; unos minutos como mucho si hay que comprar una IP nueva).

curl -X POST https://api.hooka.to/v1/senders \
  -H "Authorization: Bearer $HOOKA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: connect-sales-es-1" \
  -d '{ "label": "Ventas ES #1", "country": "ES" }'

# → 201
{
  "id": "snd_5f0c2a8e-7c1b-4b8e-9d3a-2f6a1c9e4b10",
  "object": "sender",
  "platform": "tiktok",
  "username": null,
  "label": "Ventas ES #1",
  "country": "ES",
  "status": "awaiting_scan",
  "status_reason": null,
  "health": null,
  "daily_limit": 30,
  "sent_today": 0,
  "connection": {
    "status": "pending",
    "qr_code": null,
    "verification": null,
    "websocket_url": "wss://api.hooka.to/ws/sessions/0b6f7d7e-3f7e-4a0e-8a51-1f2e3d4c5b6a",
    "error": null
  },
  "exit_ip": { "status": "provisioning", "country": null, "fallback": false, "dedicated": true },
  "created": 1790760000
}

Consulta el sender cada 3–5 segundos y actúa según connection.status. El webhook sender.connection.qr_generated te avisa de que el primer QR está listo (y lo incluye), pero es la señal para empezar a consultar, no un sustituto: el QR también puede cambiar sin ningún evento, así que sigue consultando mientras lo muestras.

export SENDER_ID="snd_5f0c2a8e-7c1b-4b8e-9d3a-2f6a1c9e4b10"

curl https://api.hooka.to/v1/senders/$SENDER_ID \
  -H "Authorization: Bearer $HOOKA_API_KEY"
  • pending: se está preparando la IP de la cuenta, arrancando el navegador, o TikTok está cargando un QR nuevo. Espera y no escanees un QR antiguo.
  • requires_scan: connection.qr_code contiene un PNG en data URI (data:image/png;base64,...). Muéstralo y escanéalo con la app de TikTok, con la sesión iniciada en la cuenta que quieres conectar. Muestra siempre el último QR: si nadie lo escanea en unos 2 minutos, se sustituye por otro.
  • scanned: TikTok detectó el escaneo; falta que el usuario confirme el inicio de sesión en el móvil.
  • verification_required: TikTok pide «Verifica que eres tú». connection.verification.step dice qué quiere: choose_method (elige un correo o un teléfono de verification.options) o enter_code (envía el código que ha recibido el usuario); a los dos se responde con …/connection/verify, más abajo. Hay otros dos pasos que no admiten ninguna acción (el endpoint los rechaza): finishing, en el que TikTok está terminando el inicio de sesión, normalmente porque aceptó el código (sigue consultando), y unsupported, en el que TikTok pide algo que Hooka no puede gestionar, como un captcha o una contraseña. Salvo que TikTok pase a un paso admitido, ese intento termina con verification_unsupported; puedes abrir otro enseguida con …/connection/refresh.
  • La conexión termina bien cuando el status del sender pasa a "connected": username se rellena, connection pasa a null y llega el webhook sender.connected. Si connection.status pasa a "expired" o "failed", o status a "disconnected", el intento ha terminado.
curl -X POST https://api.hooka.to/v1/senders/$SENDER_ID/connection/verify \
  -H "Authorization: Bearer $HOOKA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: verify-sales-es-1-code-1" \
  -d '{ "action": "submit_code", "code": "123456" }'

# Otras acciones:
#   { "action": "choose_method", "option_id": 0 }   (un id de verification.options)
#   { "action": "resend_code" }                      (cuando verification.resend_available es true)

La llamada de verificación responde 202 en cuanto la acción queda en cola, antes de que TikTok la aplique, así que sigue consultando para ver el resultado. Usa una Idempotency-Key nueva para cada código. Con un código incorrecto, el paso sigue en enter_code y el error de TikTok aparece en verification.messages. Cuando un intento falla, el motivo está en connection.error mientras el sender sigue en awaiting_scan (status_reason es null), y en status_reason cuando pasa a disconnected. POST /v1/senders/{id}/connection/refresh abre un intento nuevo.

Conecta las cuentas de una en una: cada POST /v1/senders o …/connection/refresh cancela cualquier otro intento en curso del workspace, que termina con status_reason: "superseded". Un intento dura unos 5 minutos desde el primer QR, algo más si TikTok pide verificación.

2. Crea una campaña

Una campaña nueva siempre se crea en draft: no envía nada hasta que la arrancas. Los campos principales:

  • message: un texto, o {"variants": [...]} con hasta 20 variantes de 1 a 2.000 caracteres; para cada destinatario y paso se elige una al azar.
  • follow_ups: hasta 10 pasos {"wait_days": 1–60, "variants": [...]}. wait_days cuenta desde el envío del paso anterior. Un follow-up solo sale si la persona no ha respondido, y siempre desde la cuenta que envió el paso anterior.
  • Variables: {{first_name}} o cualquier clave que pongas en las variables de los targets; {{username}} (alias {{handle}}) es el destinatario y {{my_handle}}, la cuenta que envía. Con on_missing_variable: "skip", el target al que le falte una variable se salta; con "blank" (por defecto), se envía con el hueco vacío.
  • settings.sender_ids (de 1 a 10 IDs snd_): las cuentas desde las que enviar. Si lo omites, la campaña toma hasta 10 de tus cuentas conectadas más antiguas en ese momento. Por API, el conjunto no se puede cambiar después (en el panel, sí).
  • settings.rate_limit_seconds (de 30 a 3.600; 60 por defecto): los segundos mínimos entre dos DMs de una misma cuenta.
  • settings.daily_limit (de 1 a 500, o null): tope diario de la campaña, sumando todas sus cuentas. El tope propio de cada cuenta se aplica igualmente.
  • settings.dedupe_across_campaigns (true por defecto): no escribe a quien otra de tus campañas ya le envió un DM.
curl -X POST https://api.hooka.to/v1/campaigns \
  -H "Authorization: Bearer $HOOKA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ugc-october-create" \
  -d '{
    "name": "Outreach UGC - octubre",
    "client_reference": "crm-4412",
    "message": {
      "variants": [
        "Hola {{first_name}}, me encantó tu último vídeo. ¿Te interesa una colaboración UGC pagada?",
        "¡Hola {{first_name}}! Pagamos a creadores como tú por vídeos UGC. ¿Te interesa?"
      ]
    },
    "follow_ups": [
      { "wait_days": 3, "variants": ["{{first_name}}, te vuelvo a escribir por si se te pasó mi mensaje."] }
    ],
    "settings": {
      "sender_ids": ["snd_5f0c2a8e-7c1b-4b8e-9d3a-2f6a1c9e4b10"],
      "rate_limit_seconds": 90,
      "daily_limit": 40,
      "on_missing_variable": "skip"
    }
  }'

# → 201 (la campaña más el resultado de los targets enviados en línea; aquí, ninguno)
{
  "id": "cmp_4f0c2d9e-1b7a-4c1e-9a53-2e8d7c6b5a41",
  "object": "campaign",
  "platform": "tiktok",
  "name": "Outreach UGC - octubre",
  "client_reference": "crm-4412",
  "status": "draft",
  "message": {
    "variants": [
      "Hola {{first_name}}, me encantó tu último vídeo. ¿Te interesa una colaboración UGC pagada?",
      "¡Hola {{first_name}}! Pagamos a creadores como tú por vídeos UGC. ¿Te interesa?"
    ]
  },
  "follow_ups": [
    { "wait_days": 3, "variants": ["{{first_name}}, te vuelvo a escribir por si se te pasó mi mensaje."] }
  ],
  "settings": {
    "rate_limit_seconds": 90,
    "daily_limit": 40,
    "daily_limit_per_sender": 40,
    "dedupe_across_campaigns": true,
    "on_missing_variable": "skip",
    "sender_ids": ["snd_5f0c2a8e-7c1b-4b8e-9d3a-2f6a1c9e4b10"]
  },
  "created": 1790762400,
  "targets_accepted": 0,
  "target_ids": [],
  "targets_skipped": []
}

daily_limit_per_sender es un alias obsoleto de daily_limit: pese al nombre, es el tope de la campaña, no el de cada cuenta. Usa daily_limit. Guarda el ID:

export CAMPAIGN_ID="cmp_4f0c2d9e-1b7a-4c1e-9a53-2e8d7c6b5a41"

3. Añade targets

Hasta 1.000 por llamada (también puedes pasar la misma lista como targets al crear la campaña). Cada elemento es un usuario en texto o un objeto {"username": ..., "variables": {...}}; los valores de las variables pueden ser textos, números o booleanos, y se guardan como texto.

curl -X POST https://api.hooka.to/v1/campaigns/$CAMPAIGN_ID/targets \
  -H "Authorization: Bearer $HOOKA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ugc-october-batch-1" \
  -d '{
    "targets": [
      { "username": "creator.one", "variables": { "first_name": "Mia" } },
      { "username": "creator_two", "variables": { "first_name": "Leo", "followers": 12000 } },
      { "username": "@Creator.One", "variables": { "first_name": "Mia" } },
      { "username": "bad handle!", "variables": { "first_name": "Ana" } }
    ]
  }'

# → 202
{
  "accepted": 2,
  "target_ids": [
    "tgt_9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
    "tgt_2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b"
  ],
  "skipped": [
    { "username": "@Creator.One", "reason": "duplicate_in_campaign" },
    { "username": "bad handle!", "reason": "invalid" }
  ],
  "campaign_status": "draft",
  "resumed": false
}

Hooka normaliza los usuarios (quita los espacios del principio y del final y las @ iniciales, y los pasa a minúsculas); por eso @Creator.One cuenta como duplicado de creator.one. Un usuario válido tiene de 1 a 40 caracteres entre letras ASCII (sin tildes ni eñes), números, _, . y -. Los elementos saltados nunca llegan a ser targets ni generan eventos. Los motivos: invalid (no es un usuario de TikTok válido), duplicate_in_campaign (ya está en esta campaña, en cualquier estado), suppressed (está en tu lista de no contactar) y duplicate_across_campaigns (otra de tus campañas ya le envió un DM, si la campaña deduplica).

Lo que pasa después depende del estado de la campaña. En una campaña running, los targets nuevos pueden salir en segundos: son DMs reales. En draft o paused esperan, porque añadir targets nunca arranca ni reanuda una campaña. Añadir targets a una campaña completed la vuelve a poner en marcha si alguna de sus cuentas está conectada ("resumed": true, con un nuevo campaign.started). Si ninguna lo está, sigue en completed con los targets nuevos pendientes, y la respuesta añade "resume_blocked_reason": "no_connected_sender" (un campo que solo aparece en este caso): reconecta una cuenta y llama a POST …/start o POST …/resume.

4. Arráncala

curl -X POST https://api.hooka.to/v1/campaigns/$CAMPAIGN_ID/start \
  -H "Authorization: Bearer $HOOKA_API_KEY" \
  -H "Idempotency-Key: ugc-october-start"

# → 200: la campaña completa, ya en marcha, con stats en vivo (extracto)
"status": "running",
"stats": { "total": 2, "queued": 2, "pending": 2, "processing": 0, "sent": 0, "replied": 0, "failed": 0, "skipped": 0, "reply_rate": 0 }

No hace falta cuerpo. start necesita al menos una cuenta de la campaña conectada (si no, 400 no_sender_available) y funciona desde draft, paused o completed con targets pendientes (si no, 409 invalid_campaign_state). POST …/pause (desde running) y POST …/resume (desde paused, o desde completed con targets pendientes) se llaman igual. Son DMs reales. A partir de aquí trabaja el motor:

  • Orden: los targets salen en el orden en que los añadiste, con los follow-ups que ya tocan por delante. Cada cuenta conectada de la campaña coge un target cada vez, así que los envíos se reparten entre hasta 10 cuentas.
  • Espaciado: al menos rate_limit_seconds entre dos DMs de la misma cuenta, y nunca menos de 30 segundos.
  • Topes diarios: cada cuenta tiene su propio tope para todas sus campañas, y daily_limit limita además la campaña (consulta límites y ritmo de envío).
  • Follow-ups: los envía la misma cuenta que envió el paso anterior, y solo si la persona no ha respondido. Si esa cuenta está desconectada, el follow-up la espera.
  • Ciclo de vida: si todas las cuentas de una campaña en marcha se desconectan, la campaña se pausa sola, sin webhook, y sigue pausada hasta que llames a resume (o a start). completed significa que ahora mismo no queda nada pendiente; la campaña vuelve a ponerse en marcha, con un nuevo campaign.started, cuando toca un follow-up, cuando le añades targets teniendo alguna cuenta conectada o cuando llamas a start o resume y aún le quedan targets pendientes.
  • Progreso: GET /v1/campaigns/{id} devuelve stats en vivo. sent cuenta filas de targets, pasos de follow-up incluidos; replied cuenta personas, y reply_rate es respondidas entre personas contactadas. GET /v1/campaigns/{id}/targets?status=failed lista los fallos.

¿Qué significa «sent» y cómo fallan los envíos?

Cada target pasa de pending a processing y termina en sent, failed o skipped. Cada paso de follow-up es un target propio, con su propio ID tgt_ y un número de step. Síguelos con los webhooks target.sent, target.failed y target.skipped, o lístalos con GET /v1/campaigns/{id}/targets?status=failed. Una respuesta no cambia el estado del target, que sigue en sent: la notifica el evento target.replied.

Para cada target, el motor abre el perfil del destinatario, pulsa Mensaje, pega el texto y lo envía. sent significa que TikTok aceptó el mensaje: el cuadro de texto se vació al enviarlo y no apareció ningún aviso de «no entregado» en unos 8 segundos. No es un acuse de entrega ni de lectura, y un primer contacto puede acabar en las solicitudes de mensajes del destinatario en lugar de en su bandeja principal. sent_at es la hora en que Hooka registró el envío.

El bloqueo de contenido de TikTok (blocked_by_platform) se detecta en cualquier idioma. Los avisos de «no entregado», incluido el de privacidad (recipient_privacy), se reconocen por su texto en el idioma de la interfaz de la cuenta, y están cubiertos el español, el inglés, el portugués, el francés, el italiano y el alemán. Un aviso así en otro idioma, o cualquier aviso que aparezca más tarde, no se detecta y el target se queda en sent.

Los fallos llevan un código error estable y el detalle en error_message (el aviso literal de TikTok, cuando lo hay):

CódigoQué pasó¿Se reintenta?
blocked_by_platformTikTok aceptó el texto, pero no lo entregó y lo marcó en la conversación (posible incumplimiento de sus normas de la comunidad).No: el mismo texto volvería a bloquearse.
recipient_privacyTikTok indicó que los ajustes de privacidad del destinatario no admiten tu mensaje.No.
recipient_unreachableNo se pudo abrir un chat: el perfil no tiene botón de Mensaje (cuenta privada, no admite mensajes de desconocidos o ya no existe).No.
send_unconfirmedHooka no pudo confirmar el envío, o se quedó colgado más de 15 minutos. El DM puede haber salido o no; un target.sent posterior puede corregirlo.No, para no duplicar el DM.
sender_session_expiredLa sesión de la cuenta ya no era válida.No: reconecta la cuenta.
send_failedCualquier otro fallo, incluido un aviso de «no entregado» de TikTok sin motivo.No.
rate_limitedTikTok frenó a la cuenta.Sí, automáticamente: el target vuelve a pending y no genera target.failed.

Un target también puede acabar en skipped sin que se envíe nada. El reason es uno de estos: suppressed (está en tu lista de no contactar), duplicate_in_campaign, duplicate_across_campaigns, recipient_replied (follow-up cancelado porque la persona respondió), sender_unavailable (la cuenta que debía enviar el follow-up se quitó de la campaña o se eliminó con DELETE /v1/senders/{id}), missing_variable o empty_message.

Límites y ritmo de envío

ControlÁmbitoValorDónde se cambia
Tope diario de la cuentaUna cuenta, en todas sus campañas30 por defecto, hasta 100Panel (no por API)
Calentamiento (opcional)Una cuentaDe 10 al día hasta el tope en 14 díasPanel
settings.daily_limitUna campaña, sumando todas sus cuentasDe 1 a 500, o null (sin tope de campaña)Al crear la campaña, o en el panel
settings.rate_limit_secondsCada cuenta, entre dos DMsDe 30 a 3.600 s; 60 por defecto. Se ignora si en el panel pasas la campaña al reparto por franja horariaAl crear la campaña, o en el panel
Pausa tras un aviso de TikTokUna cuenta5 minutosAutomática
Peticiones a la APITodo el workspace300 por minuto—

Los topes diarios cuentan intentos, también los bloqueados o sin confirmar, salvo cuando no se llegó a escribir nada. El sent_today de un sender solo cuenta los envíos correctos, así que una cuenta puede parar por hoy antes de que sent_today llegue a su daily_limit. El día se cuenta en UTC. Con una suscripción activa no hay tope mensual de DMs.

¿Cómo funcionan los webhooks?

Registra un endpoint HTTPS accesible desde internet y elige los eventos que quieres (o ["*"] para todos, incluidos los tipos que se añadan más adelante):

curl -X POST https://api.hooka.to/v1/webhook_endpoints \
  -H "Authorization: Bearer $HOOKA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: webhook-prod-1" \
  -d '{
    "url": "https://example.com/hooka/webhooks",
    "enabled_events": ["target.sent", "target.failed", "target.skipped", "target.replied", "campaign.completed", "message.received"]
  }'

# → 201
{
  "id": "we_5c0d8f4e-1b2a-4c3d-8e9f-0a1b2c3d4e5f",
  "object": "webhook_endpoint",
  "url": "https://example.com/hooka/webhooks",
  "enabled_events": ["target.sent", "target.failed", "target.skipped", "target.replied", "campaign.completed", "message.received"],
  "status": "enabled",
  "secret": "whsec_2b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da5",
  "created": 1790781000
}

El secret de firma solo se devuelve en esta respuesta: ni GET ni PATCH lo vuelven a mostrar, así que guárdalo. Hay 12 tipos de evento:

EventoCuándo se envía
sender.connection.qr_generatedEl primer QR de un intento de conexión está listo, para un sender que creaste o refrescaste por API; se vuelve a enviar cada vez que Hooka recarga la página del QR o el inicio de sesión vuelve a la pantalla del QR. Otros cambios del QR actualizan connection.qr_code sin evento, así que consulta GET /v1/senders/{id} para tener el último QR mientras lo muestras. Incluye el sender con connection.qr_code.
sender.connection.verification_requiredTikTok pide «Verifica que eres tú» tras el escaneo, o la verificación pasa a otro paso (choose_method, enter_code o unsupported; no finishing). Los dos primeros se responden con …/connection/verify.
sender.connectedUn inicio de sesión terminó y el sender está connected.
sender.connection_failedUn intento de conexión abierto por API terminó sin éxito; connection.error dice por qué (por ejemplo, connection_expired, verification_timeout o superseded). Cancelar tú el intento con DELETE /v1/senders/{id} no emite ningún evento.
sender.status_changedUn sender conectado pasó a desconectado: sesión caducada, o desconexión desde el panel o la API. Añade previous_status y reason.
campaign.startedUna campaña pasó a running desde draft o completed: start desde draft; start o resume de una campaña completed a la que aún le quedan targets pendientes; targets añadidos a una campaña completed; follow-ups que tocan. No se envía al pausar una campaña ni al reanudarla (o arrancarla) desde paused.
campaign.completedA una campaña en marcha o pausada no le queda nada pendiente ni en curso. Puede volver a enviarse cuando la campaña se reactiva.
target.sentSe envió un DM o un paso de follow-up (consulta qué significa «sent»).
target.failedUn envío falló, o se quedó colgado más de 15 minutos (send_unconfirmed). error lleva el código.
target.skippedUn target se resolvió sin enviarse; reason lleva el código.
target.repliedEl destinatario respondió después de tu DM. Como mucho una vez por target; añade replied_at, conversation_id y opt_out.
message.receivedCualquier DM entrante nuevo en cualquier conversación sincronizada, no solo respuestas de campaña. Añade sender_id, username y opt_out.

Todos los eventos target.* llevan el objeto target más sender_id, la cuenta que lo gestionó (null cuando no intervino ninguna, como en un salto causado por tu lista de no contactar).

No hay evento cuando una campaña se pausa (la pauses tú o se pause sola) ni cuando se reanuda desde paused, cuando se añaden targets ni cuando una respuesta que envías por API sale o falla: para detectar una pausa, consulta GET /v1/campaigns/{id}. Reabrir una campaña completed es distinto: arrancarla o reanudarla cuando aún le quedan targets pendientes, o añadirle targets teniendo alguna de sus cuentas conectada, sí emite campaign.started.

Un mensaje entrante que contenga la palabra suelta «stop», «unsubscribe», «baja» o «desuscribir» (en cualquier parte del texto, así que «estoy de baja esta semana» también cuenta) llega con opt_out: true y, en cuanto se conoce el usuario de la conversación, Hooka añade a esa persona a tu lista de no contactar y salta sus targets pendientes.

Cómo llega una entrega

Cada entrega es un POST cuyo cuerpo es el objeto del evento, el mismo que devuelve GET /v1/events. Así llega una respuesta a tu DM:

POST /hooka/webhooks HTTP/1.1
Content-Type: application/json
Hooka-Signature: t=1790781503,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Hooka-Event-Id: evt_6f1c2a4e-8a55-4c47-9a0e-3b1f0d2c9e11
Hooka-Event-Type: target.replied
User-Agent: Hooka-Webhooks/1.0

{
  "id": "evt_6f1c2a4e-8a55-4c47-9a0e-3b1f0d2c9e11",
  "object": "event",
  "type": "target.replied",
  "api_version": "2026-06-01",
  "created": 1790781500,
  "workspace_id": "ws_3f1c2a9e-7b4d-4c1e-9a0b-5d6e7f8a9b0c",
  "data": {
    "object": {
      "id": "tgt_9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
      "object": "target",
      "campaign_id": "cmp_4f0c2d9e-1b7a-4c1e-9a53-2e8d7c6b5a41",
      "username": "creator.one",
      "variables": { "first_name": "Mia" },
      "status": "replied",
      "step": 0,
      "sent_at": 1790765000,
      "error": null,
      "error_message": null,
      "sender_id": "snd_5f0c2a8e-7c1b-4b8e-9d3a-2f6a1c9e4b10",
      "replied_at": 1790781500,
      "conversation_id": "con_7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a",
      "opt_out": false
    }
  }
}

"status": "replied" solo existe en este evento: el target sigue en sent en GET …/targets, que no permite filtrar por respondidos. El orden de las claves dentro de data puede variar; no dependas de él.

Cómo verificar la firma

Cada entrega va firmada con HMAC-SHA256: v1 es el HMAC en hexadecimal de t + "." + cuerpo en crudo, con el secreto completo, prefijo whsec_ incluido, como clave, y t es la hora de ese intento de entrega (se recalcula en cada reintento). Verifícala sobre los bytes en crudo (si vuelves a serializar el JSON, la firma no cuadra) y rechaza las firmas cuyo t se aleje más de 5 minutos de tu reloj. En Node.js con Express:

import crypto from 'node:crypto';
import express from 'express';

function verifyHookaSignature(rawBody, header, secret, toleranceSec = 300) {
  if (!header) return false;
  const parts = {};
  for (const kv of header.split(',')) {
    const i = kv.indexOf('=');
    if (i > 0) parts[kv.slice(0, i).trim()] = kv.slice(i + 1).trim();
  }
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !parts.v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
  const got = Buffer.from(parts.v1, 'hex');
  return got.length === expected.length && crypto.timingSafeEqual(got, expected);
}

const app = express();

// express.raw conserva los bytes exactos que firmó Hooka.
app.post('/hooka/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  // HOOKA_WEBHOOK_SECRET es el "whsec_..." completo.
  if (!verifyHookaSignature(req.body, req.get('Hooka-Signature'), process.env.HOOKA_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  res.sendStatus(200); // contesta en menos de 10 s y procesa después
  const event = JSON.parse(req.body.toString('utf8'));
  // La entrega es al menos una vez: sáltate los event.id que ya hayas procesado.
});

app.listen(3000);

Reglas de entrega

  • Solo cuenta como entregado un 2xx en menos de 10 segundos. Las redirecciones no se siguen, y cualquier otra respuesta, 4xx incluido, se reintenta.
  • Los reintentos llegan al cabo de 1 min, 5 min, 30 min, 2 h, 6 h, 24 h y 24 h tras cada fallo: 8 intentos como máximo, durante unos 2,4 días. Después, la entrega se da por fallida. No hay reenvío manual, la API no expone el registro de entregas (la página Developers de tu panel muestra las 20 últimas de cada endpoint) y un endpoint nunca se desactiva solo.
  • La entrega es al menos una vez y sin orden garantizado: deduplica por el id del evento (igual a la cabecera Hooka-Event-Id) y no dependas del orden de llegada.
  • Cada evento es una foto del objeto en el momento en que ocurrió, no su estado actual.
  • La URL tiene que ser HTTPS, apuntar a un dominio público (nada de IPs privadas ni nombres internos) y no llevar usuario ni contraseña. No publicamos una lista de IPs de origen: la garantía es la firma.
  • Prueba tu receptor con POST /v1/webhook_endpoints/{id}/test, que envía un evento ping firmado solo a ese endpoint.

Alternativa: consultar GET /v1/events

¿No puedes alojar un endpoint o quieres una red de seguridad? Consulta GET /v1/events. Guarda los mismos 12 tipos de evento (más los ping de prueba) durante 30 días, y se puede filtrar con type=. Toma el evento más reciente como cursor inicial y, a partir de ahí, lee hacia delante, del más antiguo al más reciente:

# Cursor inicial: el id del evento más reciente
curl "https://api.hooka.to/v1/events?limit=1" \
  -H "Authorization: Bearer $HOOKA_API_KEY"

# Después, en bucle: los eventos posteriores al cursor, del más antiguo al más reciente
export LAST_EVENT_ID="evt_6f1c2a4e-8a55-4c47-9a0e-3b1f0d2c9e11"   # data[0].id de la llamada anterior
curl "https://api.hooka.to/v1/events?after=$LAST_EVENT_ID&limit=100" \
  -H "Authorization: Bearer $HOOKA_API_KEY"

Procesa data en orden y mueve tu cursor al último ID recibido. Mientras has_more sea true, vuelve a pedir enseguida con after=<next_cursor> (nunca con starting_after); si no, espera unos segundos. La consulta hacia delante solo devuelve eventos con más de 2 segundos de antigüedad, así que los más recientes aparecen en la siguiente consulta. Sin after, la lista va del más reciente al más antiguo y starting_after pagina hacia atrás.

Un workspace nuevo aún no tiene eventos, así que la primera llamada devuelve "data": [] y no hay cursor del que partir (after= necesita el ID de un evento que exista; si no, recibes 404 not_found). En ese caso, consulta GET /v1/events sin after cada pocos segundos hasta que aparezca el primer evento, y entonces pasa a after=.

¿Qué recursos expone la API?

La API cubre el ciclo completo de outreach: conectar cuentas, lanzar campañas, suprimir contactos, leer y responder conversaciones y recibir eventos.

RecursoEndpointsPara qué sirve
WorkspaceGET /v1/workspaceQuién eres, tus plazas libres (seats) y el país por defecto de las cuentas nuevas (exit_ip.region).
SendersPOST · GET /v1/senders
GET · PATCH · DELETE /v1/senders/{id}
POST /v1/senders/{id}/connection/refresh
POST /v1/senders/{id}/connection/verify
Conectar cuentas por QR, completar «Verifica que eres tú», ver su estado y su status_reason, cambiarles la etiqueta (label) y desconectarlas. DELETE desconecta la cuenta (sigue en la lista y se puede reconectar) y cancela los follow-ups pendientes que dependían de ella. Estados: awaiting_scan, connected, disconnected (cooldown y banned están reservados).
CampañasPOST · GET /v1/campaigns
GET /v1/campaigns/{id}
POST /v1/campaigns/{id}/start · /pause · /resume
Crear (siempre en draft), leer con stats en vivo, arrancar, pausar y reanudar. Hasta 20 variantes por paso y 10 pasos de follow-up de 1 a 60 días.
TargetsPOST · GET /v1/campaigns/{id}/targetsAñadir hasta 1.000 por llamada, con variables por target y motivos de salto, y listarlos con filtro por status. Cada paso de follow-up es un target con su propio ID.
SupresionesPOST · GET /v1/suppressions
DELETE /v1/suppressions/{username}
Tu lista de no contactar, compartida con el panel. Añade hasta 1.000 usuarios por llamada; sus targets pendientes se saltan al momento.
ConversacionesGET /v1/conversations
GET /v1/conversations/{id}
GET · POST /v1/conversations/{id}/messages
El inbox sincronizado de todas tus cuentas, y respuestas desde código: POST …/messages encola un DM real desde la cuenta de esa conversación (202).
Endpoints de webhookPOST · GET /v1/webhook_endpoints
PATCH · DELETE /v1/webhook_endpoints/{id}
POST /v1/webhook_endpoints/{id}/test
Endpoints firmados con HMAC-SHA256 y un ping de prueba.
EventosGET /v1/events
GET /v1/events/{id}
Los mismos eventos que envían los webhooks, para consultar y reconciliar, durante 30 días.
API keysPOST · GET /v1/api_keys
DELETE /v1/api_keys/{id}
Crear claves (live o test), listarlas y revocarlas.

La referencia completa, con cada campo, estado y código de error, está en la documentación de la app: app.hooka.to/docs.html.

La API no tiene un precio aparte: Hooka se paga por cuenta de TikTok conectada, con la API incluida (consulta los precios). Con una suscripción activa no hay tope mensual de DMs, solo los topes diarios por cuenta. Sin ella puedes crear claves y leer, pero no puedes conectar cuentas y las campañas no envían nada.

Lo que la API no hace

Mejor saberlo antes de integrar:

  • No hay sandbox ni modo de prueba. Las claves hk_test_ solo leen; cualquier escritura actúa sobre cuentas reales.
  • Las campañas no se editan ni se borran por API (no existen PATCH ni DELETE /v1/campaigns/{id}), y sus cuentas no se pueden cambiar después de crearla. Eso se hace en el panel. Los textos, las cuentas, el espaciado y el tope diario que cambies allí se ven en la API, pero hay opciones solo del panel que se aplican aunque settings no las muestre: el reparto por franja horaria (sustituye a rate_limit_seconds y solo envía dentro de la franja), seguir antes del DM (sube el espaciado mínimo a 60 s y, si la cuenta llega a su tope de seguidos, puede pausar la campaña) y deduplicar solo por cuenta (dedupe_across_campaigns sigue marcando true).
  • El país de una cuenta no se puede cambiar después de conectarla (soporte de Hooka puede moverla), y el tope diario por cuenta y el calentamiento solo se configuran en el panel.
  • No hay evento cuando una campaña se pausa ni cuando se reanuda desde paused. Si todas las cuentas de una campaña en marcha se desconectan, la campaña se pausa sola, sin webhook, y no se reanuda sola al reconectar: llama a resume.
  • completed no es definitivo: la campaña vuelve a running (con otro campaign.started) cuando toca el siguiente follow-up, cuando le añades targets teniendo alguna cuenta conectada o cuando llamas a start o resume y aún le quedan targets pendientes.
  • Las respuestas que envías por API no generan eventos. POST /v1/conversations/{id}/messages devuelve 202 y la respuesta queda en cola, sin evento de enviada ni de fallida. El estado de una conversación (interesado, archivada…) es de solo lectura.
  • Una persona que falló o se saltó no se puede volver a añadir a la misma campaña, y sacarla de la lista de no contactar no reactiva sus targets.
  • Una conexión de cuenta a la vez por workspace, como se explica en el quickstart.

¿Construyes con agentes de IA? Usa el servidor MCP

El servidor MCP remoto de Hooka, en https://mcp.hooka.to/mcp, expone esta misma API como herramientas para clientes MCP. Los clientes que se conectan por OAuth 2.1 (por ejemplo, los conectores personalizados de Claude o ChatGPT) inician sesión con tu cuenta de Hooka; los que permiten cabeceras, como Claude Code o Cursor, pueden enviar Authorization: Bearer hk_live_.... Las reglas son las mismas: las claves hk_test_ solo leen, y las peticiones del MCP cuentan para el mismo límite de 300 por minuto del workspace. Un token OAuth tiene el mismo acceso que una clave hk_live_, salvo para gestionar API keys. Más detalles en la página del servidor MCP de Hooka.

Nota honesta: ningún parámetro de la API elimina el riesgo de ban. La automatización de DMs en TikTok funciona fuera de la API oficial y tiene un riesgo real. Hooka lo reduce (tope diario por cuenta de 30 por defecto y 100 como máximo, al menos 30 segundos entre dos DMs de la misma cuenta, calentamiento opcional, envíos repartidos entre hasta 10 cuentas por campaña, pausas automáticas cuando TikTok avisa de que vas demasiado rápido e IP residencial dedicada en el país de tus cuentas), pero nunca prometemos cero bans. En cuentas nuevas, empieza bajo y ve subiendo, y usa cuentas dedicadas al outreach.

Preguntas frecuentes

¿Existe una API oficial de TikTok para enviar cold DMs?

No. A junio de 2026, la única API de mensajería oficial de TikTok, la Business Messaging API, solo sirve para responder: el usuario tiene que escribir primero a tu negocio, tienes una ventana de respuesta de 48 horas con unos 10 mensajes automatizados por ventana y el envío masivo está prohibido. Toda herramienta real de cold DM, Hooka incluida, funciona fuera de la API oficial. La API de Hooka añade una capa gestionada encima (topes diarios por cuenta, espaciado entre mensajes y pausas automáticas) que reduce el riesgo, pero nunca lo elimina.

¿Cómo consigo una API key de Hooka?

La creas tú mismo en la página Developers de tu panel de Hooka. Las claves de producción empiezan por hk_live_, se guardan solo como hash sha256 y se muestran una única vez, al crearlas, así que guárdala en un sitio seguro. Crear una clave no requiere suscripción, pero conectar cuentas de TikTok y enviar DMs sí: se paga por cuenta de TikTok conectada, con la API incluida. Las claves de prueba (hk_test_) existen, pero son de solo lectura, porque Hooka no tiene sandbox.

¿Hay un modo de prueba o sandbox?

No. Nada de la API de Hooka es simulado. Las claves hk_test_ solo leen: cualquier POST, PATCH o DELETE devuelve 403 test_key_read_only. Toda escritura con una clave hk_live_ actúa sobre tus cuentas reales de TikTok, y arrancar una campaña envía DMs reales. Para probar tu receptor de webhooks, envía un evento ping firmado con POST /v1/webhook_endpoints/{id}/test. Para probar una campaña, usa una cuenta de outreach dedicada y pocos destinatarios.

¿La API envía los DMs desde mis propias cuentas de TikTok?

Sí. Conectas cada cuenta escaneando un código QR con la app de TikTok: POST /v1/senders abre el intento y GET /v1/senders/{id} te da el QR cuando connection.status es requires_scan. Hooka nunca te pide la contraseña de TikTok. Si TikTok pide «Verifica que eres tú», respondes con el código que llega por correo o SMS a través de POST /v1/senders/{id}/connection/verify. Después, los envíos salen de navegadores gestionados por Hooka con la sesión de tu cuenta, a través de la IP residencial dedicada de esa cuenta, de su país.

¿Cuántos DMs al día puede enviar cada cuenta?

Cada cuenta tiene su propio tope diario: 30 por defecto, ampliable hasta 100 desde el panel (no desde la API), con un calentamiento opcional que empieza en 10 al día y llega a tu objetivo en 14 días. Además, settings.daily_limit limita cada campaña sumando todas sus cuentas, y settings.rate_limit_seconds (de 30 a 3600, 60 por defecto) marca los segundos mínimos entre dos DMs de la misma cuenta. Los topes cuentan intentos, no solo envíos confirmados, y el día se cuenta en UTC. En cuentas nuevas, empieza bajo y ve subiendo.

¿Qué significa «sent» en la API de Hooka?

Que TikTok aceptó el mensaje: el cuadro de texto se vació al enviarlo y no apareció ningún aviso de «no entregado» en unos 8 segundos. No es un acuse de entrega ni de lectura, y un primer contacto puede acabar en las solicitudes de mensajes del destinatario en lugar de en su bandeja principal. Si TikTok bloquea el mensaje o el perfil no lo admite, el target queda en failed con un código estable (blocked_by_platform, recipient_privacy, recipient_unreachable…). Un envío que Hooka no puede confirmar falla con send_unconfirmed y no se reintenta nunca, para no duplicar el DM.

¿Uso webhooks o consulto /v1/events?

Webhooks en producción, y la consulta de eventos como red de seguridad. Cada entrega va firmada con HMAC-SHA256, solo cuenta como entregada con un 2xx en menos de 10 segundos y, si falla, se reintenta al cabo de 1 min, 5 min, 30 min, 2 h, 6 h, 24 h y 24 h: 8 intentos como máximo, durante unos 2,4 días. La entrega es al menos una vez y sin orden garantizado, así que deduplica por el id del evento. GET /v1/events con after= devuelve los mismos eventos, del más antiguo al más reciente, y los guarda 30 días: consúltalo en desarrollo local o para reconciliar lo que se le haya escapado a un webhook.

Sigue leyendo

Envía tu primer DM de TikTok desde código

Cancela cuando quieras. No pedimos tu contraseña de TikTok. Pago seguro con Stripe.