1. Início
  2. API
API · REST v1 · JSON sobre HTTPS

A API de outreach por DM no TikTok

O TikTok não tem uma API oficial para enviar cold DMs: a Business Messaging API dele só deixa você responder a quem escreveu primeiro. A API REST do Hooka é a camada programática gerenciada para DMs de saída no TikTok: conecte suas contas do TikTok por QR, crie campanhas com variações de mensagem e follow-ups, adicione até 1.000 targets por requisição e receba os envios e as respostas por webhook assinado ou por um feed de eventos consultado por polling.

Última atualização: 30 de setembro de 2026 · Pela equipe Hooka

Existe uma API oficial do TikTok para enviar DMs?

Não. Em junho de 2026, o TikTok não oferece nenhuma API oficial para iniciar mensagens diretas com pessoas que não falaram com você antes. A única interface oficial de mensagens — a Business Messaging API — foi feita para atendimento de entrada, não para outreach.

As restrições dela deixam isso claro: o usuário precisa mandar mensagem para a sua empresa primeiro, você tem uma janela de resposta de 48 horas, pode enviar cerca de 10 mensagens automáticas por janela e envios em massa (broadcasting) são proibidos. É exatamente por isso que as ferramentas de entrada construídas sobre ela (o ManyChat é a mais conhecida) não conseguem iniciar uma única cold DM.

Então toda ferramenta de cold DM de verdade no mercado funciona fora da API oficial — o Hooka também. Dizemos isso com todas as letras porque isso define todo o design: a API do Hooka é uma camada programática gerenciada sobre contas reais do TikTok. Você chama endpoints REST limpos; o Hooka roda navegadores reais com a sessão de cada conta, conectados por um IP residencial dedicado no país das suas contas, aplica no servidor o intervalo entre mensagens e os limites diários por conta e, quando o TikTok mostra um aviso de que você está enviando rápido demais, devolve a mensagem para a fila e pausa essa conta por 5 minutos. Os números por trás desses padrões estão no nosso guia de limites de DM no TikTok em 2026.

300
requisições / minuto por workspace
1.000
targets por requisição
12
tipos de evento de webhook
24 h
de validade da chave de idempotência

O que você precisa antes da primeira chamada?

  1. Uma assinatura ativa (ou em período de teste). Seu plano define quantas contas do TikTok você pode conectar (seats). Sem assinatura, POST /v1/senders devolve 403 no_active_subscription e o motor não envia nada, mesmo que uma campanha apareça como running e a API não devolva nenhum erro.
  2. O país de cada conta. Cada conta que você conecta recebe seu próprio IP residencial dedicado no país dela, para que o TikTok não veja um login vindo de um data center. Passe country (ISO 3166-1 alfa-2) ao criar o sender, ou deixe que ele use o país padrão do seu workspace. Isso não pode ser alterado depois.
  3. Uma chave de API hk_live_. Crie-a na página Desenvolvedores do seu painel. Ela é guardada só como hash sha256 e aparece uma única vez, então copie-a ao criá-la.

Uma única chamada diz se você está pronto: seats.used precisa ser menor que seats.limit, e exit_ip.region é o país padrão que os senders novos recebem quando você não passa country.

export HOOKA_API_KEY="hk_live_..."   # sua chave, da página Desenvolvedores

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" só significa que não há um país padrão confirmado: não bloqueia nada, basta passar country ao criar o sender. Se faltar algo, POST /v1/senders responde 400 country_required, 403 no_active_subscription ou 403 sender_limit_reached, e 409 exit_ip_not_ready (com retry_after) se não houver IP disponível agora nesse país nem nos mais próximos.

Como funciona a API do Hooka?

Funciona do jeito que você espera se já usou o Stripe: REST sobre HTTPS, JSON, chaves bearer, IDs tipados, paginação por cursor e idempotência. As convenções:

  • URL base: todos os endpoints ficam em https://api.hooka.to/v1. Ela foi feita para chamadas de servidor para servidor: não coloque sua chave em código que roda no navegador.
  • Chaves: envie Authorization: Bearer hk_live_.... As chaves hk_test_ existem (POST /v1/api_keys com {"mode": "test"}), mas são só de leitura: qualquer POST, PATCH ou DELETE devolve 403 test_key_read_only. Não há sandbox: toda escrita age nas suas contas reais do TikTok.
  • Só JSON: envie todo corpo com Content-Type: application/json, ou você recebe 415 unsupported_content_type (atenção: curl -d sem esse cabeçalho envia um formulário). O corpo tem limite de 1 MB.
  • IDs tipados: cada ID diz para o que aponta — snd_ senders (contas do TikTok), cmp_ campanhas, tgt_ targets, con_ conversas, msg_ mensagens, rpl_ respostas na fila (devolvidas por POST /v1/conversations/{id}/messages; nenhum endpoint as consulta), sup_ supressões, we_ endpoints de webhook, evt_ eventos, key_ chaves de API e ws_ o seu workspace.
  • Datas e horas são inteiros em segundos Unix, em UTC (created, sent_at…).
  • Paginação: as listas devolvem {"object": "list", "data": [...], "has_more": true, "next_cursor": "cmp_..."}. Passe limit (de 1 a 100; 50 por padrão) e, enquanto has_more for true, busque a página seguinte com starting_after=<next_cursor>. Duas exceções: em GET /v1/events?after=... você continua com after=<next_cursor>, nunca com starting_after (junto com after ele dá 400 e, sozinho, devolve os eventos anteriores ao cursor, do mais recente para o mais antigo); e GET /v1/webhook_endpoints e GET /v1/api_keys não são paginados: devolvem todos os itens como {"object": "list", "data": [...]}, sem has_more nem next_cursor, e ignoram limit.
  • Idempotência: todo POST, exceto POST /v1/api_keys, aceita um cabeçalho Idempotency-Key (de 1 a 255 caracteres). As chaves ficam guardadas por 24 horas por workspace. Repetir com a mesma chave e a mesma requisição devolve a resposta guardada com Idempotent-Replayed: true, em vez de executar duas vezes. A mesma chave com uma requisição diferente dá 409 idempotency_key_reuse; enquanto a original ainda está rodando, você recebe 409 idempotency_key_in_use — tente de novo em instantes com a mesma chave. Erros 5xx, 429 e erros que dependem do estado do momento (como no_sender_available) não são guardados, então uma nova tentativa com a mesma chave executa de novo. Uma resposta repetida é uma foto da resposta original, não o estado atual.
  • Rate-limit: 300 requisições por minuto por workspace, compartilhadas entre todas as suas chaves e tokens OAuth. As respostas trazem x-ratelimit-limit, x-ratelimit-remaining e x-ratelimit-reset; acima do limite, você recebe 429 rate_limited com um cabeçalho Retry-After e retry_after no corpo.
  • Rastreamento: toda resposta traz um cabeçalho Hooka-Request-Id, repetido como error.request_id nos erros. Informe-o nos chamados de suporte.

Erros

Todo erro usa o mesmo envelope. code é o valor estável para decidir o que fazer, e param indica o campo com problema, quando houver:

{
  "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"
  }
}

O type quase sempre define o status HTTP: invalid_request_error 400, authentication_error 401, permission_error 403, not_found_error 404, conflict_error 409, rate_limit_error 429 e api_error 500. As duas exceções: invalid_request_error também vem com 413 (body_too_large) e 415 (unsupported_content_type), e api_error pode ser qualquer 5xx. Um recurso de outro workspace se comporta exatamente como um que não existe (404).

A maioria dos endpoints ignora em silêncio os campos do corpo que não conhece, então confira a grafia: uma configuração inventada como spacing_seconds é simplesmente descartada (o intervalo entre DMs é settings.rate_limit_seconds). A exceção é PATCH /v1/senders/{id}, que só aceita label e rejeita qualquer outro campo com um 400 cujo param indica esse campo. Um PATCH /v1/webhook_endpoints/{id} sem nenhum de url, enabled_events ou status também é um 400.

As campanhas que você cria pela API e as que cria no painel usam o mesmo motor de envio e aparecem nos dois lugares: todas as funcionalidades, do sender rotation ao inbox unificado, trabalham sobre os mesmos dados que as suas chamadas.

Quickstart: da chave de API a uma campanha rodando

Este é todo o fluxo principal em quatro passos: conecte um sender, crie uma campanha, adicione targets e inicie a campanha — depois, escute os eventos. Os exemplos usam a variável HOOKA_API_KEY da seção anterior; troque os IDs pelos que a sua API devolver.

1. Conecte uma conta do TikTok (sender)

Um sender é uma das suas contas do TikTok. A conexão é só por QR: o Hooka nunca pede sua senha do TikTok. POST /v1/senders cria o sender, com seu próprio IP residencial dedicado no country da conta, e abre uma tentativa de conexão, mas o QR não vem na resposta: um navegador o gera assim que o IP fica pronto, normalmente em segundos (enquanto isso, exit_ip.status é provisioning; no máximo alguns minutos quando é preciso comprar um IP novo).

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": "Vendas ES #1", "country": "ES" }'

# → 201
{
  "id": "snd_5f0c2a8e-7c1b-4b8e-9d3a-2f6a1c9e4b10",
  "object": "sender",
  "platform": "tiktok",
  "username": null,
  "label": "Vendas 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
}

Consulte o sender a cada 3–5 segundos e aja de acordo com connection.status. O webhook sender.connection.qr_generated avisa quando o primeiro QR está pronto (e o traz), mas é um sinal para começar a consultar, não um substituto: o QR também pode mudar sem nenhum evento, então continue consultando enquanto o exibe.

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 — o IP da conta está sendo preparado, o navegador está iniciando ou o TikTok está carregando um QR novo. Espere e não escaneie um QR antigo.
  • requires_scan — connection.qr_code traz um PNG como data URI (data:image/png;base64,...). Exiba-o e escaneie pelo app do TikTok, logado na conta que você quer conectar. Mostre sempre o QR mais recente: se ninguém o escanear em cerca de 2 minutos, ele é substituído.
  • scanned — o TikTok detectou a leitura; o usuário confirma o login no celular.
  • verification_required — o TikTok pediu para você confirmar que é você. connection.verification.step diz o que ele quer: choose_method (escolha um e-mail ou telefone de verification.options) ou enter_code (envie o código que o usuário recebeu) — responda aos dois com …/connection/verify, abaixo. Outros dois passos não aceitam nenhuma ação (o endpoint os rejeita): finishing — o TikTok está concluindo o login, normalmente porque o código foi aceito; continue consultando — e unsupported — o TikTok pediu algo que o Hooka não consegue tratar, como um captcha ou uma senha. A menos que o TikTok passe para um passo suportado, essa tentativa termina com verification_unsupported; você pode iniciar outra na hora com …/connection/refresh.
  • Pronto quando o status do sender for "connected": username é preenchido, connection passa a null e o webhook sender.connected é disparado. Se connection.status virar "expired" ou "failed", ou status virar "disconnected", a tentativa acabou.
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" }'

# Outras ações:
#   { "action": "choose_method", "option_id": 0 }   (um id de verification.options)
#   { "action": "resend_code" }                      (quando verification.resend_available é true)

A chamada de verificação responde 202 assim que a ação entra na fila, antes de o TikTok aplicá-la, então continue consultando para ver o resultado. Use uma Idempotency-Key nova para cada código. Um código errado mantém o passo em enter_code, com o erro do TikTok em verification.messages. Quando uma tentativa falha, o motivo fica em connection.error enquanto o sender ainda está em awaiting_scan (status_reason é null), e em status_reason quando ele passa a disconnected. POST /v1/senders/{id}/connection/refresh abre uma nova tentativa.

Conecte as contas uma de cada vez: cada POST /v1/senders ou …/connection/refresh cancela qualquer outra tentativa em andamento no workspace, que termina com status_reason: "superseded". Uma tentativa dura cerca de 5 minutos a partir do primeiro QR, mais se o TikTok pedir verificação.

2. Crie uma campanha

Uma campanha nova é sempre draft: nada é enviado até você iniciá-la. Os campos principais:

  • message: um texto, ou {"variants": [...]} com até 20 variações de 1 a 2.000 caracteres; uma delas é sorteada para cada destinatário e etapa.
  • follow_ups: até 10 etapas de {"wait_days": 1–60, "variants": [...]}. wait_days conta a partir do envio da etapa anterior. Um follow-up só sai se a pessoa não respondeu, e sempre pela conta que enviou a etapa anterior.
  • Variáveis: {{first_name}} ou qualquer chave que você colocar nas variables dos targets; {{username}} (alias {{handle}}) é o destinatário e {{my_handle}}, a conta que envia. Com on_missing_variable: "skip", um target sem alguma variável é ignorado; com "blank" (o padrão), ele é enviado com o espaço vazio.
  • settings.sender_ids (de 1 a 10 IDs snd_): as contas que vão enviar. Se você omitir, a campanha pega até 10 das suas contas conectadas mais antigas naquele momento. A API não consegue mudar esse conjunto depois (o painel consegue).
  • settings.rate_limit_seconds (de 30 a 3.600; 60 por padrão): o intervalo mínimo entre duas DMs da mesma conta.
  • settings.daily_limit (de 1 a 500, ou null): um limite diário para a campanha, somando todas as contas dela. O limite próprio de cada conta continua valendo.
  • settings.dedupe_across_campaigns (padrão true): ignora quem já recebeu uma DM de outra das suas campanhas.
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 - outubro",
    "client_reference": "crm-4412",
    "message": {
      "variants": [
        "Oi {{first_name}}, adorei seu último vídeo. Topa uma parceria UGC paga?",
        "Olá {{first_name}}, pagamos criadores como você por vídeos UGC. Tem interesse?"
      ]
    },
    "follow_ups": [
      { "wait_days": 3, "variants": ["{{first_name}}, só passando de novo caso minha mensagem tenha se perdido."] }
    ],
    "settings": {
      "sender_ids": ["snd_5f0c2a8e-7c1b-4b8e-9d3a-2f6a1c9e4b10"],
      "rate_limit_seconds": 90,
      "daily_limit": 40,
      "on_missing_variable": "skip"
    }
  }'

# → 201 (a campanha, mais o resultado dos targets enviados junto — nenhum aqui)
{
  "id": "cmp_4f0c2d9e-1b7a-4c1e-9a53-2e8d7c6b5a41",
  "object": "campaign",
  "platform": "tiktok",
  "name": "Outreach UGC - outubro",
  "client_reference": "crm-4412",
  "status": "draft",
  "message": {
    "variants": [
      "Oi {{first_name}}, adorei seu último vídeo. Topa uma parceria UGC paga?",
      "Olá {{first_name}}, pagamos criadores como você por vídeos UGC. Tem interesse?"
    ]
  },
  "follow_ups": [
    { "wait_days": 3, "variants": ["{{first_name}}, só passando de novo caso minha mensagem tenha se perdido."] }
  ],
  "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 é um alias obsoleto de daily_limit: apesar do nome, é o limite da campanha, não o de cada conta. Use daily_limit. Guarde o ID:

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

3. Adicione targets

Até 1.000 por requisição (você também pode passar a mesma lista como targets ao criar a campanha). Cada item é um username em texto ou {"username": ..., "variables": {...}}; os valores das variáveis podem ser textos, números ou booleanos e são guardados 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
}

Os usernames são normalizados (espaços nas pontas e @ inicial removidos, tudo em minúsculas), por isso @Creator.One conta como duplicado de creator.one. Um username válido tem de 1 a 40 caracteres: letras ASCII (sem acentos nem ç), números, _, . e -. Os itens ignorados nunca viram targets e não disparam evento. Os motivos: invalid (não é um username válido do TikTok), duplicate_in_campaign (já está nesta campanha, em qualquer status), suppressed (está na sua lista “Não contatar”) e duplicate_across_campaigns (outra das suas campanhas já mandou uma DM para essa pessoa, quando a campanha faz deduplicação).

O que acontece depois depende do status da campanha. Numa campanha running, os targets novos podem sair em segundos: são DMs de verdade. Em draft ou paused eles esperam, porque adicionar targets nunca inicia nem retoma uma campanha. Adicionar targets a uma campanha completed a reabre quando um dos senders dela está conectado ("resumed": true, com um novo campaign.started). Se nenhum estiver conectado, a campanha continua completed com os targets novos pendentes, e a resposta acrescenta "resume_blocked_reason": "no_connected_sender" (um campo que só aparece nesse caso): reconecte um sender e chame POST …/start ou POST …/resume.

4. Inicie a campanha

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: a campanha completa, agora rodando, com stats ao vivo (trecho)
"status": "running",
"stats": { "total": 2, "queued": 2, "pending": 2, "processing": 0, "sent": 0, "replied": 0, "failed": 0, "skipped": 0, "reply_rate": 0 }

Não precisa de corpo. start exige pelo menos um dos senders da campanha conectado (senão, 400 no_sender_available) e funciona a partir de draft, paused ou completed com targets pendentes (senão, 409 invalid_campaign_state). POST …/pause (a partir de running) e POST …/resume (a partir de paused, ou de completed com targets pendentes) são chamados do mesmo jeito. São DMs de verdade. A partir daqui, o motor assume:

  • Ordem: os targets saem na ordem em que você os adicionou, com os follow-ups que já venceram na frente. Cada sender conectado da campanha pega um target por vez, então os envios se distribuem entre até 10 senders.
  • Intervalo: pelo menos rate_limit_seconds entre duas DMs do mesmo sender, nunca menos de 30 segundos.
  • Limites diários: cada sender tem seu próprio limite, somando todas as campanhas dele, e daily_limit limita a campanha por cima (veja limites e ritmo de envio).
  • Follow-ups são enviados pela mesma conta que enviou a etapa anterior, e só se a pessoa não respondeu. Se essa conta estiver desconectada, o follow-up espera por ela.
  • Ciclo de vida: se todos os senders de uma campanha rodando se desconectarem, a campanha é pausada automaticamente — sem webhook — e fica pausada até você chamar resume (ou start). completed significa que não há nada pendente agora; a campanha reabre, com um novo campaign.started, quando follow-ups vencem, quando você adiciona targets com um dos senders dela conectado, ou quando você chama start ou resume e ela ainda tem targets pendentes.
  • Progresso: GET /v1/campaigns/{id} devolve stats ao vivo. sent conta linhas de targets, etapas de follow-up incluídas; replied conta pessoas, e reply_rate é respondidas ÷ pessoas contatadas. GET /v1/campaigns/{id}/targets?status=failed lista as falhas.

O que “sent” significa e como os envios falham?

Cada target passa de pending para processing e termina como sent, failed ou skipped. Cada etapa de follow-up é um target próprio, com seu próprio ID tgt_ e um número de step. Acompanhe-os com os webhooks target.sent, target.failed e target.skipped, ou liste-os com GET /v1/campaigns/{id}/targets?status=failed. Uma resposta não muda o status do target — ele continua sent — e é informada pelo evento target.replied.

Para cada target, o motor abre o perfil do destinatário, clica em Mensagem, cola o texto e envia. sent significa que o TikTok aceitou a mensagem: a caixa do chat ficou vazia depois do envio e nenhum aviso de “não entregue” apareceu em cerca de 8 segundos. Não é um comprovante de entrega nem de leitura, e um primeiro contato pode cair nas solicitações de mensagem do destinatário em vez da caixa principal. sent_at é quando o Hooka registrou o envio.

O bloqueio de conteúdo do TikTok (blocked_by_platform) é detectado em qualquer idioma. Os avisos de “não entregue”, incluindo o de privacidade (recipient_privacy), são reconhecidos pelo texto no idioma da interface da conta, e estão cobertos espanhol, inglês, português, francês, italiano e alemão. Um aviso desses em outro idioma, ou qualquer aviso que apareça depois, não é detectado e o target continua sent.

As falhas trazem um código error estável, com detalhes em error_message (o aviso literal do TikTok, quando houver):

CódigoO que aconteceuTenta de novo?
blocked_by_platformO TikTok aceitou o texto, mas não o entregou, e o marcou na conversa (possível violação das diretrizes da comunidade).Não: o mesmo texto seria bloqueado de novo.
recipient_privacyO TikTok disse que as configurações de privacidade do destinatário não permitem sua mensagem.Não.
recipient_unreachableNão foi possível abrir um chat: o perfil não tem o botão Mensagem (conta privada, não aceita mensagens de desconhecidos ou não existe mais).Não.
send_unconfirmedO Hooka não conseguiu confirmar o envio, ou ele ficou travado por mais de 15 minutos. A DM pode ter saído ou não; um target.sent posterior pode corrigir isso.Não, para evitar uma DM duplicada.
sender_session_expiredA sessão da conta não era mais válida.Não: reconecte o sender.
send_failedQualquer outra coisa, incluindo um aviso de “não entregue” do TikTok sem motivo informado.Não.
rate_limitedO TikTok segurou o ritmo da conta.Sim, automaticamente: o target volta para pending e não dispara target.failed.

Um target também pode terminar como skipped, sem nada ser enviado. O reason é um destes: suppressed (na sua lista “Não contatar”), duplicate_in_campaign, duplicate_across_campaigns, recipient_replied (um follow-up cancelado porque a pessoa respondeu), sender_unavailable (a conta que devia enviar o follow-up foi removida da campanha ou excluída com DELETE /v1/senders/{id}), missing_variable ou empty_message.

Limites e ritmo de envio

ControleAbrangênciaValorOnde mudar
Limite diário da contaUma conta, somando todas as campanhas dela30 por padrão, até 100Painel (não a API)
Aquecimento (opcional)Uma contaDe 10 por dia até o limite, ao longo de 14 diasPainel
settings.daily_limitUma campanha, todas as contas dela somadasDe 1 a 500, ou null (sem limite de campanha)Ao criar a campanha, ou no painel
settings.rate_limit_secondsCada conta, entre duas DMsDe 30 a 3.600 s; 60 por padrão. Ignorado se você mudar a campanha para o ritmo por janela de horário no painelAo criar a campanha, ou no painel
Pausa após um aviso do TikTokUma conta5 minutosAutomática
Requisições à APIO workspace inteiro300 por minuto—

Os limites diários contam tentativas, incluindo as bloqueadas e as não confirmadas, exceto quando nada chegou a ser digitado. O sent_today de um sender conta só os envios bem-sucedidos, então uma conta pode parar no dia antes de sent_today chegar ao daily_limit. O dia é contado em UTC. Com uma assinatura ativa, não há limite mensal de DMs.

Como funcionam os webhooks?

Registre um endpoint HTTPS na internet pública e escolha os eventos que você quer (ou ["*"] para todos, incluindo os tipos adicionados no futuro):

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
}

O secret de assinatura é devolvido só nesta resposta — nem GET nem PATCH o devolvem de novo, então guarde-o. Há 12 tipos de evento:

EventoQuando é disparado
sender.connection.qr_generatedO primeiro QR de uma tentativa de conexão está pronto, para um sender que você criou ou renovou pela API; dispara de novo sempre que o Hooka recarrega a página do QR ou o login volta para a tela do QR. Outras mudanças de QR atualizam connection.qr_code sem evento, então consulte GET /v1/senders/{id} para pegar o QR mais recente enquanto ele estiver na tela. Traz o sender com connection.qr_code.
sender.connection.verification_requiredO TikTok pede para você confirmar que é você depois da leitura, ou a verificação passa para outro passo (choose_method, enter_code ou unsupported; não finishing). Responda aos dois primeiros com …/connection/verify.
sender.connectedUm login foi concluído e o sender está connected.
sender.connection_failedUma tentativa de conexão iniciada pela API terminou sem sucesso; connection.error diz por quê (por exemplo connection_expired, verification_timeout ou superseded). Cancelar a tentativa você mesmo com DELETE /v1/senders/{id} não emite evento.
sender.status_changedUm sender conectado ficou desconectado — sessão expirada, ou desconectado pelo painel ou pela API. Acrescenta previous_status e reason.
campaign.startedUma campanha passou a running a partir de draft ou completed: start a partir do rascunho; start ou resume de uma campanha concluída que ainda tem targets pendentes; targets adicionados a uma campanha concluída; follow-ups que venceram. Não é enviado quando uma campanha é pausada, nem quando é retomada (ou iniciada) a partir de paused.
campaign.completedUma campanha rodando ou pausada não tem nada pendente nem em andamento. Pode disparar de novo depois que a campanha reabrir.
target.sentUma DM ou etapa de follow-up foi enviada (veja o que “sent” significa).
target.failedUm envio falhou, ou ficou travado por mais de 15 minutos (send_unconfirmed). error traz o código.
target.skippedUm target foi resolvido sem envio; reason traz o código.
target.repliedO destinatário respondeu depois da sua DM. No máximo uma vez por target; acrescenta replied_at, conversation_id e opt_out.
message.receivedQualquer DM nova recebida em qualquer conversa sincronizada, não só respostas de campanha. Acrescenta sender_id, username e opt_out.

Todo evento target.* traz o objeto target mais sender_id, a conta que cuidou dele (null quando nenhuma conta participou, como num pulo causado pela sua lista “Não contatar”).

Nenhum evento é disparado quando uma campanha é pausada (por você ou automaticamente) ou retomada a partir de paused, quando targets são adicionados, ou quando uma resposta que você envia pela API sai ou falha — consulte GET /v1/campaigns/{id} para detectar uma pausa. Reabrir uma campanha completed é diferente: iniciá-la ou retomá-la enquanto ainda tem targets pendentes, ou adicionar targets a ela com um dos senders dela conectado, emite campaign.started.

Uma mensagem recebida que contenha a palavra inteira “stop”, “unsubscribe”, “baja” ou “desuscribir” (em qualquer parte do texto, então “don't stop” também conta) chega com opt_out: true e, assim que o username da conversa é conhecido, o Hooka adiciona essa pessoa à sua lista “Não contatar” e ignora os targets pendentes dela.

Como é uma entrega

Cada entrega é um POST cujo corpo é o objeto do evento — o mesmo objeto que GET /v1/events devolve. É assim que chega uma resposta à sua 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" só existe neste evento: o próprio target continua sent em GET …/targets, que não consegue filtrar por respondidos. A ordem das chaves dentro de data pode variar, então não dependa dela.

Como verificar a assinatura

Toda entrega é assinada com HMAC-SHA256: v1 é o HMAC em hex de t + "." + corpo bruto, usando como chave a string completa do segredo, incluindo o prefixo whsec_, e t é a hora daquela tentativa de entrega (recalculada a cada nova tentativa). Verifique sobre os bytes brutos — um JSON serializado de novo não vai bater — e rejeite assinaturas cujo t esteja a mais de 5 minutos do seu relógio. Em Node.js com 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 mantém os bytes exatos que o Hooka assinou.
app.post('/hooka/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  // HOOKA_WEBHOOK_SECRET é a string "whsec_..." completa.
  if (!verifyHookaSignature(req.body, req.get('Hooka-Signature'), process.env.HOOKA_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  res.sendStatus(200); // responda em até 10 s e depois faça o trabalho
  const event = JSON.parse(req.body.toString('utf8'));
  // A entrega é pelo menos uma vez: pule os event.id que você já processou.
});

app.listen(3000);

Regras de entrega

  • Só conta como entregue um 2xx respondido em até 10 segundos. Redirecionamentos não são seguidos, e qualquer outro status — 4xx incluído — é tentado de novo.
  • As novas tentativas vêm 1 min, 5 min, 30 min, 2 h, 6 h, 24 h e 24 h depois de cada falha: no máximo 8 tentativas, ao longo de cerca de 2,4 dias, depois das quais a entrega é marcada como falha. Não há reenvio nem reentrega manual, a API não expõe registros de entrega (a página Desenvolvedores do seu painel mostra as últimas 20 entregas de cada endpoint) e um endpoint nunca é desativado automaticamente.
  • A entrega é pelo menos uma vez e sem ordem garantida: elimine duplicados pelo id do evento (também enviado como Hooka-Event-Id) e não dependa da ordem de chegada.
  • Cada evento é uma foto do objeto no momento em que aconteceu, não o estado atual dele.
  • A URL precisa usar HTTPS, apontar para um domínio público (sem IPs privados nem nomes internos) e não levar usuário nem senha. O Hooka não publica uma lista de IPs liberados: a garantia é a assinatura.
  • Teste seu receptor com POST /v1/webhook_endpoints/{id}/test, que envia um evento ping assinado só para esse endpoint.

Polling em vez de webhooks: GET /v1/events

Não consegue hospedar um endpoint, ou quer uma rede de segurança? Consulte GET /v1/events. Ele guarda os mesmos 12 tipos de evento (mais os pings de teste) por 30 dias, e dá para filtrar com type=. Pegue o evento mais recente como cursor inicial e depois leia para a frente, do mais antigo para o mais recente:

# Cursor inicial: o id do evento mais recente
curl "https://api.hooka.to/v1/events?limit=1" \
  -H "Authorization: Bearer $HOOKA_API_KEY"

# Depois, em loop: os eventos após o cursor, do mais antigo para o mais recente
export LAST_EVENT_ID="evt_6f1c2a4e-8a55-4c47-9a0e-3b1f0d2c9e11"   # data[0].id da chamada acima
curl "https://api.hooka.to/v1/events?after=$LAST_EVENT_ID&limit=100" \
  -H "Authorization: Bearer $HOOKA_API_KEY"

Processe data em ordem e mova seu cursor para o último ID recebido. Enquanto has_more for true, busque de novo na hora com after=<next_cursor> (nunca starting_after); senão, espere alguns segundos. O polling para a frente só devolve eventos com mais de 2 segundos, então os eventos muito recentes aparecem na consulta seguinte. Sem after, a lista vem do mais recente para o mais antigo e starting_after pagina para trás.

Um workspace novo ainda não tem eventos, então a primeira chamada devolve "data": [] e não há cursor para começar (after= precisa do ID de um evento que exista, senão você recebe 404 not_found). Nesse caso, consulte GET /v1/events sem after a cada poucos segundos até o primeiro evento aparecer e, então, passe para after=.

Quais recursos a API expõe?

A API cobre o ciclo completo de outreach: conectar contas, rodar campanhas, suprimir contatos, ler e responder conversas e receber eventos.

RecursoEndpointsO que faz
WorkspaceGET /v1/workspaceQuem você é, suas vagas livres (seats) e o país padrão das contas novas (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 contas por QR, concluir a confirmação de identidade do TikTok, ler o status e o status_reason delas, renomeá-las (label) e desconectá-las. DELETE desconecta a conta (ela continua na lista e pode ser reconectada) e cancela os follow-ups pendentes que dependiam dela. Status: awaiting_scan, connected, disconnected (cooldown e banned são reservados).
CampanhasPOST · GET /v1/campaigns
GET /v1/campaigns/{id}
POST /v1/campaigns/{id}/start · /pause · /resume
Criar (sempre como draft), ler com stats ao vivo, iniciar, pausar e retomar. Até 20 variações por etapa e 10 etapas de follow-up de 1–60 dias.
TargetsPOST · GET /v1/campaigns/{id}/targetsAdicionar até 1.000 por requisição, com variáveis por target e motivos de pulo, e listá-los com filtro por status. Cada etapa de follow-up é um target com seu próprio ID.
SupressõesPOST · GET /v1/suppressions
DELETE /v1/suppressions/{username}
Sua lista “Não contatar”, compartilhada com o painel. Adicione até 1.000 usernames por requisição; os targets pendentes deles são ignorados na hora.
ConversasGET /v1/conversations
GET /v1/conversations/{id}
GET · POST /v1/conversations/{id}/messages
O inbox sincronizado de todas as suas contas, e respostas a partir do código: POST …/messages coloca na fila uma DM de verdade pela conta daquela conversa (202).
Endpoints de webhookPOST · GET /v1/webhook_endpoints
PATCH · DELETE /v1/webhook_endpoints/{id}
POST /v1/webhook_endpoints/{id}/test
Endpoints assinados com HMAC-SHA256, mais um ping de teste.
EventosGET /v1/events
GET /v1/events/{id}
Os mesmos eventos que os webhooks enviam, para polling e conciliação, guardados por 30 dias.
Chaves de APIPOST · GET /v1/api_keys
DELETE /v1/api_keys/{id}
Criar chaves (live ou test), listá-las e revogá-las.

A referência completa — cada campo, status e código de erro — está na documentação do app (em inglês), em app.hooka.to/docs.html.

A API não tem preço à parte: o Hooka é cobrado por conta do TikTok conectada, com a API incluída (veja os preços). Com uma assinatura ativa, não há limite mensal de DMs, só os limites diários por conta. Sem assinatura, você ainda pode criar chaves e ler, mas não pode conectar senders, e as campanhas não enviam nada.

O que a API não faz

Melhor saber antes de integrar:

  • Não há sandbox nem modo de teste. As chaves hk_test_ só leem; toda escrita age em contas reais.
  • As campanhas não podem ser editadas nem excluídas pela API (não existe PATCH nem DELETE /v1/campaigns/{id}), e os senders delas não podem ser trocados depois da criação. Isso é feito no painel. Os textos, senders, intervalo e limite diário que você muda lá aparecem na API, mas algumas opções exclusivas do painel valem mesmo sem aparecer em settings: o ritmo por janela de horário (substitui rate_limit_seconds e só envia dentro da janela), seguir antes da DM (sobe o intervalo mínimo para 60 s e, se a conta atingir o limite de seguidos, pode pausar a campanha) e a deduplicação por conta (dedupe_across_campaigns continua mostrando true).
  • O país de uma conta não pode ser alterado depois que você a conecta (o suporte do Hooka pode mudá-lo), e o limite diário por conta e o aquecimento só são configurados no painel.
  • Nenhum evento quando uma campanha é pausada, ou retomada a partir de paused. Se todos os senders de uma campanha rodando se desconectarem, a campanha se pausa sozinha, sem webhook, e não volta sozinha quando você reconecta: chame resume.
  • completed não é definitivo: a campanha volta a running (com outro campaign.started) quando o próximo follow-up vence, quando você adiciona targets com um dos senders dela conectado, ou quando você chama start ou resume e ela ainda tem targets pendentes.
  • As respostas que você envia pela API não disparam eventos. POST /v1/conversations/{id}/messages devolve 202 e a resposta entra na fila, sem evento de enviada nem de falha. O status de uma conversa (interessado, arquivada…) é só de leitura.
  • Quem falhou ou foi ignorado não pode ser adicionado de novo à mesma campanha, e tirar a pessoa da lista “Não contatar” não reativa os targets dela.
  • Uma conexão de conta por vez por workspace, como explicado no quickstart.

Construindo com agentes de IA? Use o servidor MCP

O servidor MCP remoto do Hooka, em https://mcp.hooka.to/mcp, expõe esta mesma API como ferramentas para clientes MCP. Os clientes que se conectam com OAuth 2.1 (por exemplo, os conectores personalizados do Claude ou do ChatGPT) fazem login com a sua conta do Hooka; os clientes que aceitam cabeçalhos, como o Claude Code ou o Cursor, podem enviar Authorization: Bearer hk_live_.... As regras são as mesmas: as chaves hk_test_ só leem, e as requisições MCP contam para o mesmo limite de 300 por minuto do workspace. Um token OAuth tem o mesmo acesso que uma chave hk_live_, exceto para gerenciar chaves de API. Mais detalhes na página do servidor MCP do Hooka.

Nota honesta: nenhum parâmetro da API elimina o risco de banimento. A automação de DMs no TikTok funciona fora da API oficial e tem um risco real. O Hooka o reduz — um limite diário por conta (30 por padrão, 100 no máximo), pelo menos 30 segundos entre duas DMs da mesma conta, aquecimento opcional, envios distribuídos entre até 10 senders por campanha, pausas automáticas quando o TikTok diz que você está indo rápido demais e um IP residencial dedicado no país das suas contas —, mas nunca prometemos zero banimentos. Comece com pouco em contas novas, vá subindo e use contas dedicadas ao outreach.

Perguntas frequentes

Existe uma API oficial do TikTok para enviar cold DMs?

Não. Em junho de 2026, a única API de mensagens do TikTok — a Business Messaging API — é só de entrada: o usuário precisa mandar mensagem para a sua empresa primeiro, você tem uma janela de resposta de 48 horas com cerca de 10 mensagens automáticas por janela, e envios em massa são proibidos. Toda ferramenta de cold DM de verdade, incluindo o Hooka, funciona fora da API oficial. A API do Hooka acrescenta uma camada gerenciada por cima — limites diários por conta, intervalo entre mensagens e pausas automáticas — que reduz o risco, mas nunca o elimina.

Como consigo uma chave de API do Hooka?

Você mesmo a cria, na página Desenvolvedores do seu painel do Hooka. As chaves live começam com hk_live_, são guardadas só como hash sha256 e aparecem uma única vez, quando você as cria, então guarde a sua em um lugar seguro. Criar uma chave não exige assinatura, mas conectar contas do TikTok e enviar DMs exige: o preço é por conta do TikTok conectada, com a API incluída. As chaves de teste (hk_test_) existem, mas são só de leitura, porque o Hooka não tem sandbox.

Existe sandbox ou modo de teste?

Não. Nada na API do Hooka é simulado. As chaves hk_test_ só conseguem ler: qualquer POST, PATCH ou DELETE devolve 403 test_key_read_only. Toda escrita feita com uma chave hk_live_ age nas suas contas reais do TikTok, e iniciar uma campanha envia DMs de verdade. Para testar seu receptor de webhooks, envie um evento ping assinado com POST /v1/webhook_endpoints/{id}/test. Para testar uma campanha, use uma conta dedicada ao outreach e poucos destinatários.

A API envia DMs pelas minhas próprias contas do TikTok?

Sim. Você conecta cada conta escaneando um QR code com o app do TikTok: POST /v1/senders inicia a tentativa e GET /v1/senders/{id} devolve o QR quando connection.status for requires_scan. O Hooka nunca pede sua senha do TikTok. Se o TikTok pedir para você confirmar que é você, responda com o código recebido por e-mail ou SMS via POST /v1/senders/{id}/connection/verify. Os envios rodam em navegadores gerenciados pelo Hooka com a sessão da sua conta, pelo IP residencial dedicado dessa conta, no país dela.

Quantas DMs por dia cada conta pode enviar?

Cada conta tem seu próprio limite diário: 30 por padrão, ajustável até 100 no painel (não pela API), com um aquecimento opcional que começa em 10 por dia e chega à sua meta em 14 dias. Além disso, settings.daily_limit limita cada campanha somando todas as contas dela, e settings.rate_limit_seconds (de 30 a 3600, 60 por padrão) define os segundos mínimos entre duas DMs da mesma conta. Os limites contam tentativas, não só envios confirmados, e o dia é contado em UTC. Comece com pouco em contas novas e vá subindo.

O que “sent” significa na API do Hooka?

Que o TikTok aceitou a mensagem: a caixa do chat ficou vazia depois do envio e nenhum aviso de “não entregue” apareceu em cerca de 8 segundos. Não é um comprovante de entrega nem de leitura, e um primeiro contato pode cair nas solicitações de mensagem do destinatário em vez da caixa principal. Quando o TikTok bloqueia a mensagem ou o perfil não a aceita, o target fica failed com um código estável (blocked_by_platform, recipient_privacy, recipient_unreachable…). Um envio que o Hooka não consegue confirmar falha com send_unconfirmed e nunca é tentado de novo, para evitar uma DM duplicada.

Devo usar webhooks ou fazer polling em /v1/events?

Use webhooks em produção e mantenha o polling como rede de segurança. As entregas são assinadas com HMAC-SHA256, só contam como entregues com um 2xx respondido em até 10 segundos e são tentadas de novo 1 min, 5 min, 30 min, 2 h, 6 h, 24 h e 24 h depois de cada falha: no máximo 8 tentativas, ao longo de cerca de 2,4 dias. A entrega é pelo menos uma vez e sem ordem garantida, então elimine duplicados pelo id do evento. GET /v1/events com after= devolve os mesmos eventos, do mais antigo para o mais recente, e os guarda por 30 dias: consulte-o durante o desenvolvimento local ou para conciliar o que um webhook tenha perdido.

Continue lendo

Envie sua primeira DM no TikTok por código

Cancele quando quiser. Não pedimos sua senha do TikTok. Pagamento seguro com Stripe.