- Home
- MCP server
Run TikTok DM outreach from your LLM
The Hooka MCP server gives Claude, ChatGPT, Cursor or your own agent 26 tools over the Hooka API. With them it can connect TikTok accounts by QR, create and launch DM campaigns, work the inbox and manage your Do-Not-Contact list. Use the hosted server at mcp.hooka.to (sign in with OAuth or send an API key), or run it locally over stdio with npx hooka-mcp.
What is the Hooka MCP server?
The Hooka MCP server is a Model Context Protocol server. It exposes Hooka's REST API v1 as tools your LLM can call. Every tool maps to exactly one endpoint of https://api.hooka.to/v1 and forwards your own credential (an API key or an OAuth token). The server stores no credentials, and the same server-side limits apply as when you call the API directly.
On top of the plain API calls, the server does a few things for the model:
- It returns the connection QR code as an image, so the assistant can show it to you.
- It sends an idempotency key on the write tools that support one, so a retried call doesn't create a second campaign or send a second DM.
- It turns API errors into one readable line with the error code, the field at fault and the request id.
Tool titles and descriptions, server instructions, prompts, resources, and the notes and errors the server writes itself (missing key, timeout, network error) are in Spanish. Picking a prompt adds a Spanish message to the conversation. Errors returned by the Hooka API come through unchanged, in English, as do schema validation errors (-32602).
How do I connect Hooka to Claude, ChatGPT or Cursor?
There are three ways to connect. All of them give access to the same 26 tools:
| Option | Where it runs | Credential | Typical clients |
|---|---|---|---|
| Hosted + OAuth | https://mcp.hooka.to/mcp (Streamable HTTP) | Sign in to Hooka and approve access | Claude.ai, Claude Desktop (Connectors), ChatGPT |
| Hosted + API key | https://mcp.hooka.to/mcp (Streamable HTTP) | Header Authorization: Bearer hk_live_… | Claude Code, Cursor, custom agents |
| Local | Your machine, over stdio (npx hooka-mcp, Node.js 20+) | Env var HOOKA_API_KEY | Claude Desktop config file, Cursor, any stdio client |
Hosted, with OAuth (Claude.ai, Claude Desktop, ChatGPT)
Add Hooka as a custom connector and paste this URL. There is nothing to install and no key to copy:
https://mcp.hooka.to/mcp
The client discovers Hooka's authorization server, registers itself and sends you to Hooka to sign in and approve access (OAuth 2.1, PKCE S256 required). Then the tools appear. Some details worth knowing:
- The approval gives full access to your workspace, the same as a live API key. The one exception is API key management, which OAuth tokens can't do.
- Access tokens last 30 minutes. The client renews them with a refresh token, which lasts 60 days and is rotated on every use.
- The sign-in and consent screen is in Spanish only.
Hosted, with an API key (Claude Code, Cursor, custom agents)
Clients that can send HTTP headers can skip OAuth and send your key. In Claude Code:
claude mcp add --transport http hooka https://mcp.hooka.to/mcp --header "Authorization: Bearer hk_live_..."
In Cursor (mcp.json) and other clients that use the url + headers format:
{
"mcpServers": {
"hooka": {
"url": "https://mcp.hooka.to/mcp",
"headers": { "Authorization": "Bearer hk_live_..." }
}
}
}
The hosted server doesn't check the key when you connect. An invalid or revoked key still lists the tools, and then every tool call returns 401 authentication_error/invalid_api_key. To check your setup, ask the assistant to call get_workspace.
Local, over stdio (npx)
The npm package hooka-mcp runs the same server on your machine, speaking stdio. Add this to your client's MCP config:
{
"mcpServers": {
"hooka": {
"command": "npx",
"args": ["-y", "hooka-mcp"],
"env": { "HOOKA_API_KEY": "hk_live_..." }
}
}
}
In Claude Desktop this goes in claude_desktop_config.json. That file only loads local servers, so use a connector for the hosted URL. In Cursor, use the same command, args and env. Requires Node.js 20 or later. The local server reads these environment variables:
HOOKA_API_KEY(required): your key. Without it the server still starts, but every tool returns an error asking for the key.HOOKA_API_BASE(optional): the API base URL. Defaulthttps://api.hooka.to.HOOKA_API_TIMEOUT_MS(optional): the timeout for each API call. Default30000.
This page documents hooka-mcp 0.3.0, the version behind the hosted server. Older 0.1.x releases have 23 tools and don't include the QR image, the verification tool or the retry behaviour described here. Before you run it locally, check which version npx will fetch with npm view hooka-mcp version. If it prints 0.1.x, 0.2.0 isn't on npm yet: use the hosted server until it is. On connect, the server also reports its version to your client (serverInfo).
Which credential should the agent use?
| Credential | How you get it | What the agent can do |
|---|---|---|
hk_live_… | The Developers page of your Hooka dashboard (it always creates live keys), or POST /v1/api_keys | Read and write: real actions on your real TikTok accounts |
hk_test_… | POST /v1/api_keys with {"mode":"test"}, authenticated with a live key | Read only: every tool that writes returns 403 permission_error/test_key_read_only |
| OAuth token | Issued when you approve the connector (hosted server only) | Read and write, like a live key. It can't manage API keys. |
There is no sandbox. A test key reads your real workspace; it doesn't simulate sending. It works well when an agent should only look: reporting, inbox summaries, campaign stats. To create one, call the API with a live key:
curl https://api.hooka.to/v1/api_keys \
-H "Authorization: Bearer hk_live_..." \
-H "Content-Type: application/json" \
-d '{"mode":"test","label":"agent read-only"}'
{
"id": "key_6a1e…",
"object": "api_key",
"mode": "test",
"prefix": "hk_test_1a2b3c",
"secret": "hk_test_1a2b3c…"
}
The secret is shown only once. With a test key, reads work, including get_workspace. Every write fails the same way, whatever the tool:
403 permission_error/test_key_read_only: Test keys (hk_test_) are read-only: Hooka has no sandbox, so any write would act on your real TikTok accounts. Use an hk_live_ key to create, start or send. [request_id req_…]
All the keys and OAuth tokens of a workspace share its rate limit of 300 API requests per minute, together with any direct API traffic.
What can the agent do? The 26 tools
Each tool calls one endpoint of the Hooka API v1. Read tools carry the MCP readOnlyHint annotation. Tools marked destructive carry destructiveHint because they send real DMs, cut connections or re-allow contact. Clients that honour annotations can ask you before running them.
| Tool | What it does | API call | Kind |
|---|---|---|---|
| Senders (TikTok accounts) · 7 | |||
list_senders | Every TikTok account (sender) in the workspace, in any status, with status_reason, health, daily_limit and sent_today. Optional status filter. | GET /v1/senders | read |
get_sender | One sender. This is the tool to poll during a connection: while connection.status is requires_scan, the QR comes back as an image. | GET /v1/senders/{id} | read |
create_sender | Starts connecting a TikTok account by QR (no password). country is the account's country: it gets its own dedicated residential IP there, and the country can't be changed later. It returns the sender in awaiting_scan with no QR yet. It takes a seat and cancels any other connection in progress in the workspace. | POST /v1/senders | destructive |
refresh_sender_connection | A new connection attempt (new QR) for a sender that isn't connected: expired, disconnected, cooldown or banned. It keeps the sender's own IP. | POST /v1/senders/{id}/connection/refresh | destructive |
verify_sender_connection | Completes TikTok's "Verify it's you" step after the scan: it chooses email or phone, submits the code or asks for a new one. | POST /v1/senders/{id}/connection/verify | write |
update_sender | Changes a sender's label, the only field the API can edit. | PATCH /v1/senders/{id} | write |
delete_sender | Disconnects an account. The sender stays listed as disconnected and its seat is freed. It also cancels a QR connection in progress for that sender. Running campaigns left with no connected sender are paused, and pending follow-ups that only this account could send are skipped (sender_unavailable). | DELETE /v1/senders/{id} | destructive |
| Campaigns · 8 | |||
list_campaigns | Campaigns, newest first. Archived ones aren't listed. | GET /v1/campaigns | read |
get_campaign | One campaign with live stats: pending, processing, sent, failed, skipped, replied, reply_rate. | GET /v1/campaigns/{id} | read |
create_campaign | Creates a draft. It takes a message or up to 20 variants, up to 10 follow-ups, up to 1,000 targets, up to 10 senders and pacing settings. It sends nothing. | POST /v1/campaigns | write |
add_targets | Adds up to 1,000 targets per call. On a running campaign they start getting DMs within seconds. A completed campaign reopens (resumed: true) if one of its senders is connected. If none is, the response carries resume_blocked_reason: "no_connected_sender": reconnect a sender, then call start_campaign or resume_campaign. | POST /v1/campaigns/{id}/targets | destructive |
list_targets | Targets in the order they were added, with their status and a stable error code when one failed or was skipped. Optional status filter. | GET /v1/campaigns/{id}/targets | read |
start_campaign | Starts sending real DMs. Works on a draft, a paused campaign, or a completed one that still has pending targets. | POST /v1/campaigns/{id}/start | destructive |
pause_campaign | Pauses a running campaign. A DM already being sent can still go out. | POST /v1/campaigns/{id}/pause | write |
resume_campaign | Resumes sending on a paused campaign, or a completed one that still has pending targets. | POST /v1/campaigns/{id}/resume | destructive |
| Inbox · 4 | |||
list_conversations | Conversations by latest activity. Optional status and sender_id filters. | GET /v1/conversations | read |
get_conversation | One conversation: which sender, the other person, status, unread count and last message. | GET /v1/conversations/{id} | read |
get_messages | The messages of a conversation, oldest first (in = them, out = you). | GET /v1/conversations/{id}/messages | read |
send_reply | Queues a reply of 1 to 2,000 characters. It goes out as a real DM from the conversation's sender within seconds. | POST /v1/conversations/{id}/messages | destructive |
| Do-Not-Contact list · 3 | |||
list_suppressions | The usernames that must never be contacted, with their reason (manual, opt_out…). | GET /v1/suppressions | read |
add_suppressions | Adds 1 to 1,000 usernames. Their pending targets in every campaign are skipped right away. | POST /v1/suppressions | write |
remove_suppression | Removes one username. That can re-allow DMs to someone who asked to stop. | DELETE /v1/suppressions/{username} | destructive |
| Events, webhooks and workspace · 4 | |||
list_events | Workspace events, the same ones webhooks deliver, kept for 30 days. Browse newest first, or poll new events with after. | GET /v1/events | read |
list_webhook_endpoints | Your webhook endpoints, without their secrets. | GET /v1/webhook_endpoints | read |
create_webhook_endpoint | Creates an endpoint (public https:// URL). It returns the whsec_… signing secret once. | POST /v1/webhook_endpoints | write |
get_workspace | Whoami: workspace id, default country for new accounts (exit_ip.region) and seats. | GET /v1/workspace | read |
Paginated lists take limit (1-100, default 50) and starting_after (the previous page's next_cursor). Pass it while has_more is true. list_webhook_endpoints isn't paginated.
Examples
Connect a TikTok account
You say: "Connect my TikTok account and label it Shop ES." The agent then goes through these steps:
get_workspace: it checks that a seat is free (seats.used < seats.limit; anulllimit means no cap) and reads the default country (exit_ip.region). If the account is from another country, the agent asks you.create_sender {"label":"Shop ES","country":"ES"}: the sender comes back inawaiting_scanwithconnection.status"pending"and no QR yet. The account gets its own dedicated residential IP in that country; while it's being prepared,exit_ip.statusis"provisioning".get_senderevery 3-5 seconds.connection.statusmoves throughpending,requires_scan,scanned, sometimesverification_required, and ends with the senderconnected.- At
requires_scan, the tool result carries the QR as an image, and you scan it with the TikTok app. The QR renews itself; the agent shows the latest one. - At
verification_required, it asks you where to receive the code and what the code is. Then it callsverify_sender_connection, for example{"id":"snd_…","action":"submit_code","code":"482913"}.
This is what a get_sender result looks like while the QR is ready (abridged):
{
"content": [
{ "type": "text", "text": "{ \"id\": \"snd_…\", \"object\": \"sender\", \"status\": \"awaiting_scan\", \"connection\": { \"status\": \"requires_scan\", \"qr_code\": \"[QR adjunto como imagen en esta respuesta: …]\", … } }" },
{ "type": "image", "data": "iVBORw0KGgo…", "mimeType": "image/png" }
]
}
The attempt has failed if connection.status becomes expired or failed (connection.error says why), or if the sender becomes disconnected (status_reason says why). The agent stops polling and can offer refresh_sender_connection for a new QR. Connect accounts one at a time, because each new attempt cancels the one in progress.
Create a campaign, then start it only after you confirm
You say: "Draft a campaign for these two creators with a follow-up after 3 days, 90 seconds between DMs and at most 40 a day." The agent calls:
{
"name": "create_campaign",
"arguments": {
"name": "Creators ES - October",
"message": {
"variants": [
"Hi {{first_name}}, loved your latest video. Open to a paid collab?",
"Hey {{first_name}}, we're looking for creators like you. Interested?"
]
},
"follow_ups": [
{ "wait_days": 3, "variants": ["{{first_name}}, did you get a chance to see my message?"] }
],
"targets": [
{ "username": "@maria.ugc", "variables": { "first_name": "Maria" } },
{ "username": "@leo_creates", "variables": { "first_name": "Leo" } }
],
"settings": { "rate_limit_seconds": 90, "daily_limit": 40 }
}
}
The result is the campaign in "status": "draft", plus targets_accepted, target_ids and targets_skipped. Nothing has been sent at this point. Some notes on the arguments:
- Without
settings.sender_ids, the campaign uses up to 10 of the oldest connected senders of the workspace. daily_limitcaps the whole campaign per day. Each account also keeps its own daily cap.- Follow-ups go out from the same account that sent the first DM, and they stop when the person replies.
Next, the agent shows you a summary and asks before it calls start_campaign {"id":"cmp_…"}. Its server instructions and the launch_campaign prompt both tell it to do that.
Triage the inbox with a read-only key
With an hk_test_ key, you can let an agent run list_conversations {"status":"open"} and get_messages, sort replies into interested, question, not interested or spam, and draft answers for you. It can't send them: send_reply returns 403 test_key_read_only. You review the drafts and send them yourself, or switch to a live key.
Check what happened overnight
"What happened in my campaigns since yesterday?" The agent reads list_events, for example with type set to target.replied or sender.status_changed, and get_campaign for the stats. The hooka://event-types resource explains what each event type means.
Prompts, resources and server instructions
Prompts (guided workflows the user can pick in clients that support them):
launch_campaign(optional argumentgoal). It checks for a connected account, proposes a message with 2-3 variants and follow-ups, creates the draft, and asks for your explicit confirmation beforestart_campaign.triage_inbox. It pages through open conversations, classifies them and drafts short replies. It waits for your OK before anysend_reply.connect_account(optional argumentlabel). It runs the QR flow above, including TikTok's verification step.
Resources (read-only text; no API call, so they work with any key):
hooka://event-typeslists the 12 webhook event types plusping, with what each one means.hooka://api-referencecovers the base URL, id prefixes, pagination, idempotency, the error format and the rate limit.
Server instructions are sent on initialize. They cover the following: test keys are read-only, which tools send real DMs and need your confirmation, the connection flow, pagination, idempotency and the error format.
What can't the agent do?
The MCP server can only do what the API allows. Some things are dashboard-only or not exposed at all:
- Billing, plan and the default country. Subscribing and changing the default country are done in the dashboard;
get_workspaceonly reads them. An account's own country is set withcreate_senderand can't be changed later (Hooka support can move it). - Per-account daily cap and warm-up. These are dashboard-only. The cap defaults to 30 DMs a day per account (adjustable up to 100). A campaign's
daily_limitcan't raise it. - Editing campaigns. No endpoint edits, archives or deletes a campaign. Through the API, a campaign's message, follow-ups and senders can't change after creation (the dashboard's campaign editor can change all of them).
- Conversation status.
open,interestedand the other statuses are read-only through the API. - Webhook maintenance. Testing, editing or deleting an endpoint, and fetching a single event by id, exist in the API but not as tools.
- API keys. There is no tool for them. OAuth tokens get
403 oauth_token_not_allowedon the key routes. - Comment-to-DM automations. They're managed in the dashboard and aren't exposed through the API.
- Settings that don't exist.
create_campaign.settingsis strict, sosender_pool,stop_on_replyor any other unknown key is a validation error. Follow-ups always stop when the person replies. - Other platforms. TikTok only.
- Push notifications. The hosted server is stateless and sends no notifications. Poll
list_events, or create a webhook endpoint for your backend.
What "sent" means. send_reply answers {"id":"rpl_…","object":"message","status":"queued"}, and the DM goes out within seconds. A target counts as sent when TikTok accepted the message and showed no "not delivered" notice in the next few seconds. It doesn't prove the person read it. When the send can't be confirmed, the target ends failed with send_unconfirmed and is never retried, to avoid a duplicate DM.
Is it safe to let an AI send DMs?
Four actions send real, irreversible DMs: start_campaign, resume_campaign, send_reply, and add_targets on a campaign that is running or completed. The server instructions tell the model to ask for your explicit confirmation before each of them.
Eight tools carry the destructiveHint annotation:
- the four above;
create_senderandrefresh_sender_connection, which cancel other connections in progress;delete_sender;remove_suppression, which can re-allow contact with someone who opted out.
Whether a client asks before a destructive tool depends on the client and your settings.
Hooka applies the same limits whoever calls it: dashboard, API or MCP.
- A daily cap per account.
- Spacing between DMs from the same account:
rate_limit_seconds, 30-3600, default 60. - An optional daily cap per campaign.
- Follow-ups that stop when the person replies.
- Deduplication across campaigns (on by default).
- The Do-Not-Contact list.
Accounts connect by QR; Hooka never asks for a TikTok password.
Honest note: TikTok DM automation runs outside TikTok's official API and carries real risk, and no tool can promise zero bans. The MCP server doesn't change that. It gives your LLM a structured way to drive Hooka with your confirmation. Use dedicated outreach accounts and start with low volumes.
Retries, idempotency and errors
These 10 write tools accept an optional idempotency_key and always send an Idempotency-Key header:
create_sender,refresh_sender_connection,verify_sender_connectioncreate_campaign,add_targets,start_campaign,pause_campaign,resume_campaignsend_reply,create_webhook_endpoint
How the key works:
- Derived key. Without
idempotency_key, the key comes from the tool name, the arguments and the current fixed 10-minute clock window (12:00-12:10, 12:10-12:20…). An identical call in the same window returns the stored result with a note instead of running again. In the next window it runs again, even seconds later. Two exceptions, where the MCP moves to the next key in the series (up to 5 per window) and the call runs again: the stored result was a 4xx error, or, forcreate_senderandrefresh_sender_connection, the stored connection attempt has already ended (the sender is no longerawaiting_scan, orconnection.statusisexpiredorfailed). So repeating the samecreate_sendercall in the same window, for another account after the first one connected, does start a new connection. An explicitidempotency_keyhas no such exceptions. - Timeouts and network errors. The error text includes the exact key that was sent, so the call can be retried with
idempotency_keyset to it: if the original finished you get its stored result instead of a second run; if it is still running you get409 idempotency_key_in_use(wait and retry with the same key). A request stuck for more than 5 minutes is treated as dead and runs again. Hooka keeps keys for 24 hours. 409 idempotency_key_in_use. The original request is still running. Wait a few seconds and retry with the same key.- Repeating on purpose. To pause again after resuming, or to ask for another verification code, pass a new
idempotency_key. - Tools without a key.
add_suppressions(naturally idempotent),update_sender,delete_senderandremove_suppressionsend none.
Tool errors come back with isError: true and a single line of text:
<status> <type>/<code>: <message> (param: <field>) [request_id req_…]
409 conflict_error/invalid_campaign_state: Cannot start a campaign that is running. [request_id req_…]
400 invalid_request_error/no_sender_available: Cannot start this campaign: none of its senders is connected. Reconnect one (POST /v1/senders/{id}/connection/refresh) and retry. [request_id req_…]
429 rate_limit_error/rate_limited: Too many requests. Retry in 7 s. (Retry-After: 7 s) [request_id req_…]
The server checks arguments against each tool's schema before calling the API. A bad argument returns MCP error -32602: Input validation error: …. Unknown top-level arguments are dropped silently. create_campaign.settings is the exception: there, an unknown key is an error. Each API call times out after 30 seconds, and the error then says the action may or may not have been applied.
Can I call the hosted server from my own code?
Yes. https://mcp.hooka.to/mcp speaks MCP Streamable HTTP in stateless mode. It follows these rules:
- Send JSON-RPC with
POST /mcp.GETandDELETEreturn 405, and there is noMcp-Session-Id. - Send
Accept: application/json, text/event-stream. Without both types you get 406. - Responses come back as server-sent events.
- Always send an
argumentsobject, even an empty one: atools/callwithout it fails validation.
curl -N https://mcp.hooka.to/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer hk_live_..." \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_workspace","arguments":{}}}'
event: message
data: {"result":{"content":[{"type":"text","text":"{\n \"id\": \"ws_…\",\n \"object\": \"workspace\",\n \"name\": \"Default\",\n \"created\": 1790726400,\n \"exit_ip\": {\n \"status\": \"ready\",\n \"region\": \"ES\",\n \"suggested_region\": null,\n \"per_sender\": true\n },\n \"seats\": {\n \"limit\": 3,\n \"used\": 1\n }\n}"}]},"jsonrpc":"2.0","id":1}
A tool result is a text item holding the API's JSON (pretty-printed), plus an image item when a QR is attached. When the API returns no body, the text is OK. On a replayed call (the same idempotent call again inside the key's 10-minute window, or a repeated idempotency_key), the first text item is a Spanish NOTA: … line, the JSON is the next text item, and no QR image is attached. For create_sender, refresh_sender_connection, verify_sender_connection, start_campaign, pause_campaign and resume_campaign, that note also carries the current state (Estado ACTUAL: …). Look for the JSON item instead of assuming content[0]. There is no structuredContent. A real client normally calls initialize first, to receive the server instructions.
Other endpoints on the hosted server:
GET https://mcp.hooka.to/healthreturns{"ok":true,"service":"hooka-mcp","version":"0.3.0","transport":"streamable-http"}.- OAuth clients discover the authorization server through
/.well-known/oauth-protected-resource/mcp. The authorization server ishttps://api.hooka.to. It supports dynamic client registration and requires PKCE S256, and the resource ishttps://mcp.hooka.to/mcp.
If you only need HTTP, you can skip MCP and call the REST API directly. Full reference: app.hooka.to/docs.html.
Frequently asked questions
What is the Hooka MCP server?
A Model Context Protocol server that exposes the Hooka v1 API as 26 tools, so Claude, ChatGPT, Cursor or your own agent can connect TikTok accounts, create and launch DM campaigns, work the inbox and manage the Do-Not-Contact list in natural language. Each tool calls one Hooka API endpoint with your credential, and the server stores no credentials. This page documents version 0.2.0 (MIT license); older 0.1.x releases on npm have 23 tools.
How do I connect Hooka to Claude, ChatGPT or Cursor?
Hosted: add https://mcp.hooka.to/mcp as a custom connector in Claude.ai, Claude Desktop or ChatGPT and sign in to Hooka (OAuth 2.1). In clients that send headers, such as Claude Code and Cursor, use the same URL with Authorization: Bearer hk_live_.... Local: run npx -y hooka-mcp over stdio with HOOKA_API_KEY set to your key (Node.js 20+). The 26 tools need hooka-mcp 0.2.0 or later: check with npm view hooka-mcp version.
Do I need an API key?
Not with the OAuth connector: you sign in to Hooka and approve access. The header option and the local server need a key: create an hk_live_ key on the Developers page of your dashboard. hk_test_ keys are read-only: every tool that writes returns 403 permission_error/test_key_read_only.
Can I try it without sending real DMs?
There is no sandbox. An hk_test_ key lets an agent read your real workspace without changing anything. With a live key, create_campaign only creates a draft and nothing is sent until start_campaign. The tools that send real DMs are start_campaign, resume_campaign, send_reply, and add_targets on a running or completed campaign.
Is it safe to let an AI send TikTok DMs?
The server marks 8 tools as destructive (they send DMs, cut connections or re-allow contact), so clients that honour MCP annotations can ask you first, and its instructions tell the model to get your explicit confirmation before sending. Hooka applies the same limits to every caller: a daily cap per account, spacing between DMs, follow-ups that stop when the person replies, and the Do-Not-Contact list. TikTok automation always carries some risk, so review each action before you confirm it.
Can the agent show me the QR code to connect an account?
Yes. create_sender starts the connection and the agent polls get_sender every 3-5 seconds. When connection.status is requires_scan, the QR comes back as an image in the tool result, and you scan it with the TikTok app. If TikTok then asks you to verify it's you, the agent completes that step with verify_sender_connection and the code you receive.
What does it cost?
The MCP server is free (MIT license). Connecting TikTok accounts and sending DMs requires an active Hooka subscription, priced per connected TikTok account. Without one, create_sender returns 403 permission_error/no_active_subscription.
Keep reading
The REST API behind every tool: senders, campaigns, targets, inbox and webhooks.
docsFull API referenceEvery endpoint, field, error code and webhook event, with examples.
pricingHooka pricingPer connected TikTok account per month. The MCP server is free.
Let your LLM run the outreach
Cancel anytime. We never ask for your TikTok password. Secure payment with Stripe.