# TProTool WhatsApp API — machine reference # Read this file only. Do not crawl /docs HTML. # Human pages: /docs (session key) and /docs/account (account key). # Base: https://whatsapi.tprotool.com/v1/whatsapp # Paths below are relative to that base. # Success JSON adds request_id. Errors: {"code","message","retryable","request_id"}. TWO KEYS. Never mix them. - tp_ins_… session key. One session. Created on the session page. Sending and that session's webhooks/media/usage. - tp_acc_… account key. All sessions of the account. The key is the secret (Authorization). A separate 14-character alias (UTC unix seconds + 4 letters/digits, e.g. 1761150600a7K3) is a username for display and indexing only. It is created with the key and cleared on revoke. Do not send the alias as the bearer token. Owner creates and reveals the key at /api-keys. New key has ZERO permissions until the owner enables them. Changing permissions changes neither the key nor the alias. Missing permission: 403 PERMISSION_DENIED. Auth header: Authorization: Bearer JSON requests: Content-Type: application/json ## Session key tp_ins_ Cannot: list/create/delete sessions, QR, start/stop/restart/logout, typing, send-gap, message list, webhook deliveries, redeliver. instance_id in a body must equal this key's session or 403 FORBIDDEN. GET /instances/{instance_id}/api-key returns key_prefix only, not the secret. POST /instances/{instance_id}/api-key/rotate returns a new tp_ins_… and kills the old key immediately. GET /instances/{instance_id} 200 instance: instance_id, display_name, control_status, engine_status, linked_phone. engine_status WORKING = WhatsApp connected and ready to send. linked_phone is digits without +. GET /instances/{instance_id}/restrictions Does not change sending and is not checked before each message. 200 timelock.active bool, timelock.until UTC "YYYY-MM-DD HH:MM:SS UTC" or null, capping.level: none | first_warning | second_warning | capped. 502 RESTRICTIONS_UNAVAILABLE = unreadable, not "no restriction". 503 ENGINE_UNAVAILABLE = session host unreachable. Timelock: temporary lock, usually on new contacts. Capping: WhatsApp rate warning; capped blocks new chats until the cycle resets. POST /messages Idempotency-Key header optional. Same key + same body returns 200 and the original message. Same key + different body: 409 IDEMPOTENCY_CONFLICT. Body: instance_id, type text|media, to, text (type text), media (type media), client_reference optional <=128, options.delay_profile optional. to: digits with country code, no + and no leading zeros. Example 9665xxxxxxxx. text required, <=8192 characters (not bytes). Longer: 422 INVALID_TEXT. delay_profile: none = 0s (urgent only, e.g. OTP), normal = 2–12s default, cautious = 25–60s, high_protection = 61–180s. 202 {success, message_id, status:queued}. Duplicate idempotent replay is 200. Accept means queued, not delivered. client_reference is echoed on GET and on event message.unknown. Media send: media.media_id OR media.url, exactly one. caption optional <=2048 or 422 INVALID_CAPTION. filename optional with url. POST /media multipart field file. 201 {media_id, mime_type, file_name, size_bytes, url, expires_at}. Outbound max 50 MB. Larger: 413 MEDIA_TOO_LARGE. 30 uploads/minute/session: 429 RATE_LIMITED + Retry-After. Reuse one media_id for many recipients. Same file + same name returns the same media_id. GET /media/{media_id} -> 302 to a temporary URL. 404 if missing/expired. Uploaded and inbound files are deleted after 1 h. Links die at expires_at. POST /messages/batch Body: instance_id, messages[] max 50. Each item: type, to, text|media, client_reference optional. 202 results[]: {index, ok, message_id, status} or {index, ok:false, error}. Idempotency-Key and delay_profile are NOT applied to batches. GET /messages/{message_id} message: message_id, instance_id, status, to, type, client_reference, provider_message_id, last_error_code, created_at, updated_at. status: - queued: waiting, cancellable - sending: handing to WhatsApp - sent: WhatsApp accepted, provider_message_id set. End of status. Delivery/read arrive only via webhook message.ack - failed: last_error_code. WORKER_DIED = our sender died twice for this message - blocked: session disconnected or WhatsApp is limiting the account - cancelled: cancelled before send - unknown: connection dropped during hand-over. last_error_code TIMEOUT or WORKER_DIED. Needs a decision ack on message.ack: 1 server, 2 device, 3 read, 4 played (audio/video). POST /messages/{message_id}/cancel Only while queued and send has not started. Else 409 CANNOT_CANCEL. Trial credit returns on cancel. 200 {message_id, status:cancelled}. POST /messages/{message_id}/unknown-decision {"decision":"sent"|"failed"} -> 200. {"decision":"resend","confirm_duplicate":true} -> 202 new_message_id queued. Without confirm_duplicate: 422 CONFIRM_DUPLICATE_REQUIRED. Resend can duplicate a message that already arrived. A resent original accepts no second decision. GET /usage usage.messages_queued for THIS session. usage.subscription: status, plan_name, max_instances, period_end. Trial also has trial_message_limit and trial_messages_used. Webhooks on this session. Created disabled. Enable with POST .../enabled {"enabled":true}. GET/POST /instances/{instance_id}/webhooks PATCH /instances/{instance_id}/webhooks/{webhook_id} replaces the whole config: always send destination_url and events. Empty client_sign_secret keeps the current secret. clear_client_sign true removes it. DELETE /instances/{instance_id}/webhooks/{webhook_id} POST body: destination_url, events[], client_sign_secret optional, ignore_status optional default true. ignore_status true: WhatsApp Status/Stories (status@broadcast) are not forwarded. 201 {webhook_id}. Events: session.status (state + linked phone), message (incoming), message.any (in and out, fromMe), message.ack, message.reaction, message.edited, message.revoked, message.unknown (API send became unknown; payload message_id, client_reference, to, type, error_code TIMEOUT|WORKER_DIED). More events exist (groups, calls, labels, polls, presence) in the session-page webhook form. Delivery POST to destination_url: Headers: X-TProTool-Event-Id, X-TProTool-Timestamp (unix seconds), and if client_sign_secret set: X-TProTool-Signature: v1=HMAC-SHA256(timestamp + "." + raw_body, secret). Body: {event_id, event, instance_id, timestamp (ms), payload}. Reply 2xx within 8s. Else retry delays 5,10,20,40,80s, max 6 attempts, then dead. 410 disables the webhook. 429 honors Retry-After up to 1h. event_id is stable across retries: store it and ignore duplicates. Inbound media in payload.media.status: ready (media_id + temporary url), too_large, failed, outgoing, unavailable. Inbound max 50 MB. ## Account key tp_acc_ Same message, media, usage, and webhook bodies as the session key. Every call needs its permission. A path without that permission is 403 PERMISSION_DENIED. The key cannot edit its own permissions. Permissions (all off at creation): sessions.read sessions.create sessions.delete sessions.qr sessions.start sessions.stop sessions.restart sessions.logout sessions.restrictions sessions.typing sessions.send_gap sessions.api_key.read sessions.api_key.rotate messages.read messages.send messages.cancel messages.unknown media.upload media.download usage.read webhooks.read webhooks.write webhooks.delete webhooks.deliveries.read webhooks.redeliver GET /instances scope sessions.read List: instance_id, display_name, control_status, engine_status, linked_phone. POST /instances scope sessions.create {"display_name":"..."} <=120 chars. 201 {instance_id}. Same plan session limit as the website. Errors include invalid_name, subscription_inactive, no_capacity. GET /instances/{instance_id} scope sessions.read. Same object as the session key. If sessions.typing is also on: typing_enabled, typing_seconds. If sessions.send_gap is also on: send_gap_enabled, send_gap_seconds. POST /instances/{instance_id}/start|stop|restart|logout scopes sessions.start, sessions.stop, sessions.restart, sessions.logout. Stop pauses the session. Logout unlinks WhatsApp and keeps the session and its key. GET /instances/{instance_id}/qr scope sessions.qr 200 image while the session is waiting for a scan. Linked session: 409 QR_NOT_READY. Logout first, then fetch a new QR, then scan with the other phone. PATCH /instances/{instance_id}/typing scope sessions.typing {"enabled":bool,"seconds":1-20} PATCH /instances/{instance_id}/send-gap scope sessions.send_gap {"enabled":bool,"seconds":1-20} GET /instances/{instance_id}/restrictions scope sessions.restrictions. Same body as the session key. DELETE /instances/{instance_id} scope sessions.delete GET /instances/{instance_id}/api-key scope sessions.api_key.read -> {api_key: full tp_ins_…} POST /instances/{instance_id}/api-key/rotate scope sessions.api_key.rotate -> new secret, old key dies now. GET /messages?page&per_page=15|25|50|100&status&instance&type&to scope messages.read Other per_page values become 25. page list: message_id, instance_id, status, to, type, last_error_code, created_at, updated_at, plus page, per_page, total. status filter: queued sending sent failed unknown blocked cancelled. type: text|media. to: digits. POST /messages and POST /messages/batch scope messages.send. instance_id in the body is required and must belong to this account. GET /messages/{message_id} scope messages.read. Any session of the account. POST /messages/{message_id}/cancel scope messages.cancel. POST /messages/{message_id}/unknown-decision scope messages.unknown. POST /media scope media.upload. multipart file AND instance_id. GET /media/{media_id}?instance_id=ins_… scope media.download. GET /usage scope usage.read. No instance_id: queue count for the whole account. ?instance_id=: that session's queue. subscription is returned either way. Webhooks: same paths as the session key. GET list scope webhooks.read. POST create, PATCH update, POST enabled scope webhooks.write. DELETE scope webhooks.delete. GET /instances/{instance_id}/webhooks/{webhook_id}/deliveries scope webhooks.deliveries.read Latest attempts: delivery_id, event, status, attempts, last_error, received_at, sent_at. POST .../deliveries/{delivery_id}/redeliver scope webhooks.redeliver Retries a failed or dead delivery of the same event. 409 webhook_disabled if that webhook is off. 404 if not found. ## Errors 401 UNAUTHORIZED missing/invalid key. 403 ACCOUNT_SUSPENDED account suspended. 403 FORBIDDEN session key used on another session, or instance_id does not match the session key. Account-only paths reject tp_ins_. 403 PERMISSION_DENIED account key lacks that permission. 403 SUBSCRIPTION_INACTIVE no active subscription. 403 SUBSCRIPTION_EXPIRED period ended. 403 TRIAL_MESSAGE_LIMIT trial message credit used up. 404 NOT_FOUND / MEDIA_NOT_FOUND. 409 IDEMPOTENCY_CONFLICT, CANNOT_CANCEL, QR_NOT_READY (account QR while already linked). 413 MEDIA_TOO_LARGE. 422 VALIDATION_ERROR, INVALID_TO, INVALID_MEDIA, INVALID_TEXT, INVALID_CAPTION, CONFIRM_DUPLICATE_REQUIRED. 429 RATE_LIMITED. Honor Retry-After. 502 RESTRICTIONS_UNAVAILABLE. 503 ENGINE_UNAVAILABLE or STORE_FAILED. retryable true: safe to repeat the same request later. Keep request_id for support. Billing, profile, and this account key's permissions are website-only, not API.