- Início
- API
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.
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.
O que você precisa antes da primeira chamada?
- Uma assinatura ativa (ou em período de teste). Seu plano define quantas contas do TikTok você pode conectar (
seats). Sem assinatura,POST /v1/sendersdevolve403 no_active_subscriptione o motor não envia nada, mesmo que uma campanha apareça comorunninge a API não devolva nenhum erro. - 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. - 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 chaveshk_test_existem (POST /v1/api_keyscom{"mode": "test"}), mas são só de leitura: qualquer POST, PATCH ou DELETE devolve403 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ê recebe415 unsupported_content_type(atenção:curl -dsem 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 porPOST /v1/conversations/{id}/messages; nenhum endpoint as consulta),sup_supressões,we_endpoints de webhook,evt_eventos,key_chaves de API ews_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_..."}. Passelimit(de 1 a 100; 50 por padrão) e, enquantohas_morefor true, busque a página seguinte comstarting_after=<next_cursor>. Duas exceções: emGET /v1/events?after=...você continua comafter=<next_cursor>, nunca comstarting_after(junto comafterele dá 400 e, sozinho, devolve os eventos anteriores ao cursor, do mais recente para o mais antigo); eGET /v1/webhook_endpointseGET /v1/api_keysnão são paginados: devolvem todos os itens como{"object": "list", "data": [...]}, semhas_morenemnext_cursor, e ignoramlimit. - Idempotência: todo POST, exceto
POST /v1/api_keys, aceita um cabeçalhoIdempotency-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 comIdempotent-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ê recebe409 idempotency_key_in_use— tente de novo em instantes com a mesma chave. Erros 5xx, 429 e erros que dependem do estado do momento (comono_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-remainingex-ratelimit-reset; acima do limite, você recebe429 rate_limitedcom um cabeçalhoRetry-Aftereretry_afterno corpo. - Rastreamento: toda resposta traz um cabeçalho
Hooka-Request-Id, repetido comoerror.request_idnos 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_codetraz 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.stepdiz o que ele quer:choose_method(escolha um e-mail ou telefone deverification.options) ouenter_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 — eunsupported— 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 comverification_unsupported; você pode iniciar outra na hora com…/connection/refresh.- Pronto quando o
statusdo sender for"connected":usernameé preenchido,connectionpassa anulle o webhooksender.connectedé disparado. Seconnection.statusvirar"expired"ou"failed", oustatusvirar"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_daysconta 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 nasvariablesdos targets;{{username}}(alias{{handle}}) é o destinatário e{{my_handle}}, a conta que envia. Comon_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 IDssnd_): 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, ounull): 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ãotrue): 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_secondsentre 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_limitlimita 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(oustart).completedsignifica que não há nada pendente agora; a campanha reabre, com um novocampaign.started, quando follow-ups vencem, quando você adiciona targets com um dos senders dela conectado, ou quando você chamastartouresumee ela ainda tem targets pendentes. - Progresso:
GET /v1/campaigns/{id}devolvestatsao vivo.sentconta linhas de targets, etapas de follow-up incluídas;repliedconta pessoas, ereply_rateé respondidas ÷ pessoas contatadas.GET /v1/campaigns/{id}/targets?status=failedlista 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ódigo | O que aconteceu | Tenta de novo? |
|---|---|---|
blocked_by_platform | O 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_privacy | O TikTok disse que as configurações de privacidade do destinatário não permitem sua mensagem. | Não. |
recipient_unreachable | Nã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_unconfirmed | O 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_expired | A sessão da conta não era mais válida. | Não: reconecte o sender. |
send_failed | Qualquer outra coisa, incluindo um aviso de “não entregue” do TikTok sem motivo informado. | Não. |
rate_limited | O 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
| Controle | Abrangência | Valor | Onde mudar |
|---|---|---|---|
| Limite diário da conta | Uma conta, somando todas as campanhas dela | 30 por padrão, até 100 | Painel (não a API) |
| Aquecimento (opcional) | Uma conta | De 10 por dia até o limite, ao longo de 14 dias | Painel |
settings.daily_limit | Uma campanha, todas as contas dela somadas | De 1 a 500, ou null (sem limite de campanha) | Ao criar a campanha, ou no painel |
settings.rate_limit_seconds | Cada conta, entre duas DMs | De 30 a 3.600 s; 60 por padrão. Ignorado se você mudar a campanha para o ritmo por janela de horário no painel | Ao criar a campanha, ou no painel |
| Pausa após um aviso do TikTok | Uma conta | 5 minutos | Automática |
| Requisições à API | O workspace inteiro | 300 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:
| Evento | Quando é disparado |
|---|---|
| sender.connection.qr_generated | O 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_required | O 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.connected | Um login foi concluído e o sender está connected. |
| sender.connection_failed | Uma 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_changed | Um sender conectado ficou desconectado — sessão expirada, ou desconectado pelo painel ou pela API. Acrescenta previous_status e reason. |
| campaign.started | Uma 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.completed | Uma campanha rodando ou pausada não tem nada pendente nem em andamento. Pode disparar de novo depois que a campanha reabrir. |
| target.sent | Uma DM ou etapa de follow-up foi enviada (veja o que “sent” significa). |
| target.failed | Um envio falhou, ou ficou travado por mais de 15 minutos (send_unconfirmed). error traz o código. |
| target.skipped | Um target foi resolvido sem envio; reason traz o código. |
| target.replied | O destinatário respondeu depois da sua DM. No máximo uma vez por target; acrescenta replied_at, conversation_id e opt_out. |
| message.received | Qualquer 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
iddo evento (também enviado comoHooka-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 eventopingassinado 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.
| Recurso | Endpoints | O que faz |
|---|---|---|
| Workspace | GET /v1/workspace | Quem você é, suas vagas livres (seats) e o país padrão das contas novas (exit_ip.region). |
| Senders | POST · GET /v1/sendersGET · PATCH · DELETE /v1/senders/{id}POST /v1/senders/{id}/connection/refreshPOST /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). |
| Campanhas | POST · GET /v1/campaignsGET /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. |
| Targets | POST · GET /v1/campaigns/{id}/targets | Adicionar 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ões | POST · GET /v1/suppressionsDELETE /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. |
| Conversas | GET /v1/conversationsGET /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 webhook | POST · GET /v1/webhook_endpointsPATCH · DELETE /v1/webhook_endpoints/{id}POST /v1/webhook_endpoints/{id}/test | Endpoints assinados com HMAC-SHA256, mais um ping de teste. |
| Eventos | GET /v1/eventsGET /v1/events/{id} | Os mesmos eventos que os webhooks enviam, para polling e conciliação, guardados por 30 dias. |
| Chaves de API | POST · GET /v1/api_keysDELETE /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
PATCHnemDELETE /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 emsettings: o ritmo por janela de horário (substituirate_limit_secondse 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_campaignscontinua mostrandotrue). - 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: chameresume. completednão é definitivo: a campanha volta arunning(com outrocampaign.started) quando o próximo follow-up vence, quando você adiciona targets com um dos senders dela conectado, ou quando você chamastartouresumee ela ainda tem targets pendentes.- As respostas que você envia pela API não disparam eventos.
POST /v1/conversations/{id}/messagesdevolve202e 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
Sender rotation, inbox unificado, teste A/B — o motor que as suas chamadas à API comandam.
mcpServidor MCP do HookaComande senders, campanhas e o inbox pelo Claude, ChatGPT ou Cursor, sobre a mesma API.
preçosPreços do HookaPreço por conta do TikTok conectada, com a API incluída.
guiaLimites de DM no TikTok em 2026Os limites diários estimados pela comunidade e como os limites por conta do Hooka se encaixam neles.
Envie sua primeira DM no TikTok por código
Cancele quando quiser. Não pedimos sua senha do TikTok. Pagamento seguro com Stripe.