CLI reference
Every hookchat command with its usage, flags and examples, generated from docs/cli/reference.md (itself written by hookchat docs generate). Install the binary from the downloads page.
Global flags#
These flags are accepted by every command.
| Flag | Type | Default | Description |
|---|---|---|---|
--api-key | string | API key (hookchat_live_... or hookchat_test_...); overrides HOOKCHAT_API_KEY and the profile | |
--base-url | string | API origin; overrides HOOKCHAT_BASE_URL and the profile (default https://hookchat.dev) | |
--no-color | bool | disable colour even when stdout is a terminal | |
-o, --output | string | table | output format: table, json or yaml |
--profile | string | config profile to use; overrides HOOKCHAT_PROFILE (default: the config's default profile, else "default") | |
-q, --quiet | bool | print only ids or values, nothing else on success | |
--tenant | string | workspace slug, required for an operator key; overrides HOOKCHAT_TENANT and the profile | |
--timeout | duration | 30s | per-request timeout |
-y, --yes | bool | answer yes to the question before revoke, delete, rotate and erase |
Commands#
hookchat#
The HookChat command line: Instagram and Messenger DMs as webhooks
hookchat [command]hookchat is the command line for the HookChat API. Every /v1 resource has a command: keys, webhooks and their deliveries, conversations, messages, accounts, the audit trail, stats and realtime tickets. It also verifies captured webhook deliveries offline and manages login profiles.
Authentication resolves in this order: the --api-key, --base-url and --tenant flags, then the HOOKCHAT_API_KEY, HOOKCHAT_BASE_URL and HOOKCHAT_TENANT environment variables, then the profile selected by --profile or HOOKCHAT_PROFILE (or the default profile) in the config file written by "hookchat auth login". The config file lives at $HOOKCHAT_CONFIG_DIR/config.json or <user config dir>/hookchat/config.json and is created with mode 0600.
Output is a table by default. --output json prints the API resource (a list is a JSON array, or {"items": [...], "next_cursor": "..."} when a page follows) and --output yaml prints the same shape as YAML. With --output json a failure is one JSON object on stderr, {"error": {"code", "message", "detail", "request_id", "docs", "exit"}}, so a script can parse it. --quiet prints only ids, or the secret for keys create, webhooks create and webhooks rotate. Colour is used only when stdout is a terminal, NO_COLOR is unset and --no-color is not given.
0 success
1 unexpected error, or a no to the question before revoke, delete, rotate or erase
2 usage error: unknown command, flag or argument, or an erase with no terminal and no --yes
3 authentication: unauthorized, forbidden, tenant_required, or no API key configured
4 not found: a route, conversation, message, endpoint, key, delivery, upload or hold (hold_not_found)
5 invalid_request: a malformed body or parameter; attachment_too_large or unsupported_media_type: a file the gateway will not take; verification_failed: the check on a hold did not pass
6 send policy: window_closed, human_agent_unavailable or missing_actor
7 rate_limited: the send budget is exhausted or the gateway returned 429
8 send_failed, replay_conflict, replay_expired, or hold_expired: nobody passed the check in time
9 webhook signature verification failed (verify)
10 network error, timeout (an upload or hold wait too) or a 5xx from the gateway (internal_error, or no envelope)# Check the gateway and your credentials
hookchat auth login --api-key hookchat_test_... --tenant acme
hookchat ping
# Register a webhook and capture its secret
hookchat webhooks create --url https://example.com/hookchat --output json
# Reply inside the 24 hour window
hookchat messages reply --conversation "CONV#acme#instagram#1#2" --text "On it"Subcommands
- hookchat accounts: List or disconnect the workspace's linked channel accounts
- hookchat attachments: Upload files from your computer to send
- hookchat audit: Read the workspace's audit trail
- hookchat auth: Log in, inspect and remove credential profiles
- hookchat completion: Print a shell completion script
- hookchat conversations: Read the inbox
- hookchat docs: Generate and check the command reference
- hookchat keys: Create, list and revoke API keys
- hookchat messages: Read the message feed and send replies
- hookchat ping: Check that the gateway is reachable
- hookchat realtime: Issue websocket connect tickets
- hookchat stats: Show the workspace's overview counts
- hookchat verify: Verify a captured webhook delivery offline
- hookchat version: Print the CLI version
- hookchat webhooks: Manage webhook endpoints and their deliveries
hookchat accounts#
List or disconnect the workspace's linked channel accounts
hookchat accounts [command]The Instagram and Messenger accounts linked to the workspace, with their credential health. An account with a refresh_error needs relinking in the console.
# Linked accounts
hookchat accounts list
# Only the account keys
hookchat accounts list --quiet
# Disconnect one
hookchat accounts disconnect 'instagram#17841400000000001'Subcommands
- hookchat accounts disconnect: Disconnect a linked account
- hookchat accounts list: List linked accounts and their credential health
hookchat accounts disconnect#
Disconnect a linked account
hookchat accounts disconnect <account_key>Calls DELETE /v1/accounts/:account_key. Meta stops sending the account's messages, the stored token is deleted and account.disconnected fires; its conversations stay, and connecting it again brings them back. At a terminal the command asks first; --yes skips the question, and a piped stdin never asks. An unknown key, or one from another workspace, is account_not_found (exit 4). Quote the key: the # starts a comment in most shells.
# Disconnect by account key
hookchat accounts disconnect 'instagram#17841400000000001'
# Disconnect every account that needs relinking, without asking
hookchat accounts list --output json | jq -r '.[] | select(.refresh_error) | .account_key' | xargs -n1 hookchat accounts disconnect --yeshookchat accounts list#
List linked accounts and their credential health
hookchat accounts listCalls GET /v1/accounts. The list is not paginated. Each row carries the platform, the external id, the handle, the status (active, disabled or error) and the last refresh error, if any.
# Linked accounts as a table
hookchat accounts list
# Find accounts that need relinking
hookchat accounts list --output json | jq '.[] | select(.refresh_error)'hookchat attachments#
Upload files from your computer to send
hookchat attachments [command]Upload a file for a conversation and get back an attachment id to send. HookChat keeps the file only until Meta has it, and never longer than a day. To upload and send in one step, use "messages reply --attachment image=@./photo.jpg".
# Upload a photo, then send it by id
hookchat attachments upload --conversation "CONV#acme#instagram#1#2" --file ./photo.jpg
# See where an upload has got to
hookchat attachments get upl_01J9Z8Y7X6W5V4T3S2R1Q0P9N8Subcommands
- hookchat attachments get: Show where an upload has got to
- hookchat attachments upload: Upload a file and print its attachment id
hookchat attachments get#
Show where an upload has got to
hookchat attachments get <upload_id>Calls GET /v1/attachments/uploads/{upload_id} and prints the upload: its status, and once Meta has the file, the attachment id to send. The status is waiting (the file is not finished), processing (HookChat is handing it to Meta), ready or failed. A failed upload shows the error code and message. HookChat keeps an upload for a day; after that, or for another workspace's id, it is upload_not_found (exit 4).
"attachments upload" waits for you; use this when it gave up after --wait-timeout, or to check an upload from a script.
# Where an upload has got to
hookchat attachments get upl_01J9Z8Y7X6W5V4T3S2R1Q0P9N8
# Only the attachment id, once it is ready
hookchat attachments get upl_01J9Z8Y7X6W5V4T3S2R1Q0P9N8 --quiet
# As JSON
hookchat attachments get upl_01J9Z8Y7X6W5V4T3S2R1Q0P9N8 --output jsonhookchat attachments upload#
Upload a file and print its attachment id
hookchat attachments upload [flags]Uploads a file for a conversation in three steps: asks for an upload url (POST /v1/attachments/uploads), posts the file to it, then finishes (POST /v1/attachments), which hands the file to Meta and deletes HookChat's copy. The finish answers at once while HookChat hands the file to Meta, so the command waits until Meta has it, for up to --wait-timeout (2m), with a line on stderr while it waits. It prints the attachment: send its id with "messages reply" as the attachment_id of an attachment, through the API or an SDK. If the wait runs out, the command exits 10 and the upload goes on: check it with "attachments get".
The name and content type come from the file name unless you set them. Meta sets the limits: an image is png or jpeg (gif on Messenger) up to 8 MB; a video is mp4, mov, webm, ogg or avi and audio is aac, m4a, wav or mp4, each up to 25 MB; a file is a PDF up to 25 MB on Instagram, or any type but a program on Messenger. Too big is attachment_too_large and a type the account does not take is unsupported_media_type (exit 5); a file Meta refuses is send_failed (exit 8).
# Upload a photo
hookchat attachments upload --conversation "CONV#acme#instagram#1#2" --file ./photo.jpg
# A PDF, printing only the attachment id
hookchat attachments upload --conversation "CONV#acme#instagram#1#2" --file ./quote.pdf --quiet
# As JSON
hookchat attachments upload --conversation "CONV#acme#instagram#1#2" --file ./clip.mp4 --type video --output json
# A big video on a slow link: wait up to five minutes
hookchat attachments upload --conversation "CONV#acme#instagram#1#2" --file ./clip.mp4 --wait-timeout 5mFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--content-type | string | the media type, such as image/jpeg (default: from the file name) | |
--conversation | string | conversation id the file will be sent in (required) | |
--file | string | path of the file to upload (required) | |
--filename | string | the name Meta sees (default: the file's own name) | |
--type | string | how the file will be sent: image, video, audio or file (default: from the content type) | |
--wait-timeout | duration | 2m0s | how long to wait for Meta to have an uploaded file, such as 90s or 5m |
hookchat audit#
Read the workspace's audit trail
hookchat audit [command]The audit trail records every admin action in the workspace: key creation and revocation, webhook changes and replays, sign-ins, invites and membership changes. A message send is not an entry; its send attempt is the record. Entries never carry message content, tokens or secrets.
# The most recent 50 entries
hookchat audit list
# Everything, as JSON
hookchat audit list --all --output jsonSubcommands
- hookchat audit list: List audit entries, most recent first
hookchat audit list#
List audit entries, most recent first
hookchat audit list [flags]Calls GET /v1/audit. One page is printed by default; the next_cursor goes to stderr in table mode and into the JSON or YAML envelope otherwise. --all walks every page.
# One page of 20
hookchat audit list --limit 20
# Continue from a cursor
hookchat audit list --cursor eyJ...
# Every entry
hookchat audit list --allFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--all | bool | walk every page instead of printing one page and its next_cursor | |
--cursor | string | resume from a previous next_cursor | |
--limit | int | 50 | page size, 1 to 100 (with --all, the page size used while walking) |
hookchat auth#
Log in, inspect and remove credential profiles
hookchat auth [command]Profiles store an API key, a base URL and a workspace in the config file so the other commands need no flags. The file is $HOOKCHAT_CONFIG_DIR/config.json or <user config dir>/hookchat/config.json, created with mode 0600. A profile is selected with --profile or HOOKCHAT_PROFILE; otherwise the config's default profile, and "default" when the config names none. Flags and environment variables always override a profile.
# Store a key as the default profile
hookchat auth login --api-key hookchat_test_... --tenant acme
# Keep a second workspace in its own profile
hookchat auth login --profile acme --tenant acme --api-key hookchat_live_...
hookchat --profile acme pingSubcommands
- hookchat auth login: Validate an API key and store it as a profile
- hookchat auth logout: Remove a profile from the config file
- hookchat auth status: Show the active profile and ping the gateway
hookchat auth login#
Validate an API key and store it as a profile
hookchat auth login [flags]Reads the key from --api-key, or from standard input when the flag is absent (a prompt is shown when stdin is a terminal, so the key never lands in shell history). The key and base URL are validated with a ping, then written to the profile named by --profile (default "default") together with --base-url and --tenant. The first profile written becomes the default profile; pass --set-default to make any later profile the default.
The full key is never printed; status shows a masked prefix only.
# Prompt for the key, so it never lands in shell history
hookchat auth login --tenant acme
# Read the key from a secret manager without echoing it
op read op://vault/hookchat/key | hookchat auth login --tenant acme
# A script that already holds the key in a variable
hookchat auth login --api-key "$HOOKCHAT_API_KEY" --tenant acme
# A second workspace profile, made the default
hookchat auth login --profile acme --tenant acme --set-defaultFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--set-default | bool | make this profile the default profile |
hookchat auth logout#
Remove a profile from the config file
hookchat auth logoutDeletes the profile named by --profile (or HOOKCHAT_PROFILE, or the default profile) from the config file. The key itself is not revoked; use "hookchat keys revoke" for that. Removing the default profile makes the next remaining profile the default.
# Remove the default profile
hookchat auth logout
# Remove a named profile
hookchat auth logout --profile acmehookchat auth status#
Show the active profile and ping the gateway
hookchat auth statusShows which profile is active, where the key came from (flags, environment or the profile), the masked key prefix, the base URL and the workspace, then pings the gateway. The exit code follows the ping, so status doubles as a health check in scripts.
# Human readable status
hookchat auth status
# Machine readable, including the ping result
hookchat auth status --output jsonhookchat completion#
Print a shell completion script
hookchat completion bash|zsh|fish|powershellPrints a completion script for the named shell. Load it in your shell's startup file to complete commands and flags:
bash source <(hookchat completion bash)
zsh hookchat completion zsh > "${fpath[1]}/_hookchat"
fish hookchat completion fish > ~/.config/fish/completions/hookchat.fish
powershell hookchat completion powershell | Out-String | Invoke-Expression# Bash, for the current session
source <(hookchat completion bash)
# Zsh, installed permanently
hookchat completion zsh > "${fpath[1]}/_hookchat"hookchat conversations#
Read the inbox
hookchat conversations [command]A conversation is one thread between a linked account and a participant. The gateway computes each thread's messaging window (open_24h, human_agent_only or closed) and the can_reply and can_send_as_human_agent flags; you never recompute them. The participant's name, username and picture are looked up just after the first message arrives, theirs or one sent from the app; participant_profile says where that stands: pending (not run yet), complete (stored, and a participant.updated event went out) or unavailable (Meta shared nothing; tried again on their next message). Conversation ids contain "#" separators, so quote them in the shell.
# Threads waiting for an answer
hookchat conversations list --output json | jq '.[] | select(.unanswered)'
# One thread with its recent messages
hookchat conversations get "CONV#acme#instagram#1#2"Subcommands
- hookchat conversations erase: Erase one person's data
- hookchat conversations get: Show one conversation and its recent messages
- hookchat conversations list: List conversations, most recently active first
hookchat conversations erase#
Erase one person's data
hookchat conversations erase <id>Calls DELETE /v1/conversations/:id, for when your customer asks you to delete their data. A conversation is one person on one of your accounts, so this erases the conversation, its messages both ways, the stored profile, the delivered copies of its events on all your endpoints, its send records and any upload still waiting for Meta. It prints counts, never content, and it cannot be undone. At a terminal the command asks first; with no terminal it needs --yes, or nothing is sent and it exits 2. A big conversation that does not finish in about 15 seconds goes on in the background: done is false, and running the command again later prints done true once nothing is left. An id from another workspace is conversation_not_found (exit 4). Quote the id: the # starts a comment in most shells.
# Erase one person, asked first
hookchat conversations erase "CONV#acme#instagram#1#2"
# From a script
hookchat conversations erase "CONV#acme#instagram#1#2" --yes --output jsonhookchat conversations get#
Show one conversation and its recent messages
hookchat conversations get <id> [flags]Calls GET /v1/conversations/:id. The id is URL-encoded for you. The table shows the conversation's window and flags followed by its bounded thread history, with each message's media types in the MEDIA column; json and yaml print {"conversation": ..., "messages": [...], "next_cursor": ...}, and each message there carries its attachments with Meta's links. Use --limit and --cursor to read older messages, or --all for the whole thread. --include-test adds rows written by test-scope keys.
# A thread, including test-key rows
hookchat conversations get "CONV#acme#instagram#1#2" --include-test
# Whether the participant's profile has been looked up yet
hookchat conversations get "CONV#acme#instagram#1#2" --output json | jq -r .conversation.participant_profile
# Just the window state
hookchat conversations get "CONV#acme#instagram#1#2" --output json | jq -r .conversation.window.stateFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--all | bool | walk every page instead of printing one page and its next_cursor | |
--cursor | string | resume from a previous next_cursor | |
--include-test | bool | include rows written by test-scope keys | |
--limit | int | 50 | page size, 1 to 100 (with --all, the page size used while walking) |
hookchat conversations list#
List conversations, most recently active first
hookchat conversations list [flags]Calls GET /v1/conversations. One page is printed by default with the next_cursor on stderr (table) or in the envelope (json, yaml); --all walks every page. The list is the inbox feed: live rows only, newest message in either direction first. A thread your team started from the Instagram or Messenger app is listed too: LAST_INBOUND is "-" and CAN_REPLY is false.
# The first page of 50
hookchat conversations list
# Every conversation, ids only
hookchat conversations list --all --quiet
# Threads that can still be replied to
hookchat conversations list --output json | jq '.[] | select(.can_reply) | .id'Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--all | bool | walk every page instead of printing one page and its next_cursor | |
--cursor | string | resume from a previous next_cursor | |
--limit | int | 50 | page size, 1 to 100 (with --all, the page size used while walking) |
hookchat docs#
Generate and check the command reference
hookchat docs [command]The command reference is a single Markdown file generated from the command tree: every command's synopsis, description, examples and flags. It is committed at docs/cli/reference.md and a test fails when it is stale.
# Regenerate the reference after changing a command
hookchat docs generate --out docs/cli/reference.md
# Fail CI when the committed reference is stale
hookchat docs checkSubcommands
- hookchat docs check: Exit 1 when the committed reference is stale
- hookchat docs generate: Write the Markdown command reference
hookchat docs check#
Exit 1 when the committed reference is stale
hookchat docs check [flags]Generates the reference in memory and compares it with the file. A difference exits 1 with a message telling you to run "hookchat docs generate".
# Check the committed file
hookchat docs check
# Check another path
hookchat docs check --file build/reference.mdFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--file | string | docs/cli/reference.md | the reference file to compare |
hookchat docs generate#
Write the Markdown command reference
hookchat docs generate [flags]Walks the command tree and writes one Markdown file with the global flags, then a section per command with its synopsis, description, examples and flags. With --out - the reference is written to stdout.
# Write the committed reference
hookchat docs generate --out docs/cli/reference.md
# Preview on stdout
hookchat docs generate --out -Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--out | string | docs/cli/reference.md | output path, or - for stdout |
hookchat keys#
Create, list and revoke API keys
hookchat keys [command]API keys are bound to a workspace and carry a scope: live keys send real messages, test keys route sends through the mock connection and write rows that reads only see with --include-test. A key inherits the caller's scope unless --scope is given, and a test key cannot create a live key.
# Create a test key for CI
hookchat keys create --label ci --scope test
# Revoke it later
hookchat keys revoke 01ARZ3NDEKTSV4RRFFQ69G5FAVSubcommands
- hookchat keys create: Create a new API key
- hookchat keys list: List the workspace's API keys
- hookchat keys revoke: Revoke an API key
hookchat keys create#
Create a new API key
hookchat keys create [flags]Calls POST /v1/keys. The plaintext secret is returned exactly once; store it now, no read path returns it again. --quiet prints only the secret, which suits shell capture.
# A labelled live key
hookchat keys create --label "billing worker" --scope live
# Capture a test key in a script
KEY=$(hookchat keys create --label ci --scope test --quiet)Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--label | string | display label (default "key for <tenant>") | |
--scope | string | key scope: live or test (default: the caller's scope) |
hookchat keys list#
List the workspace's API keys
hookchat keys listCalls GET /v1/keys. Only the display prefix of each key is returned, never a secret. Revoked keys stay in the list with status revoked.
# All keys as a table
hookchat keys list
# Only active key ids
hookchat keys list --output json | jq -r '.[] | select(.status == "active") | .id'hookchat keys revoke#
Revoke an API key
hookchat keys revoke <id>Calls DELETE /v1/keys/:id. Anything using the key stops working at once, so at a terminal the command asks first; --yes skips the question, and a piped stdin never asks. Revoking an already revoked key succeeds; an unknown id, or one from another workspace, is key_not_found (exit 4).
# Revoke by id
hookchat keys revoke 01ARZ3NDEKTSV4RRFFQ69G5FAV
# Revoke every key with a label, quietly
hookchat keys list --output json | jq -r '.[] | select(.label == "ci") | .id' | xargs -n1 hookchat keys revoke --quiethookchat messages#
Read the message feed and send replies
hookchat messages [command]There are exactly two send paths and no generic send. "reply" works inside the 24 hour window after the participant's last message. "human-agent" works from 24 hours to 7 days and must name the human who wrote the message with --actor; it holds the message until that person passes a check at a link, then HookChat sends it. Past 7 days nothing can be sent. The gateway enforces the policy and answers window_closed, human_agent_unavailable or missing_actor (exit 6).
# Reply inside the window
hookchat messages reply --conversation "CONV#acme#instagram#1#2" --text "Thanks, on it"
# Follow up as a named human after 24 hours
hookchat messages human-agent --conversation "CONV#acme#instagram#1#2" --text "Any update?" --actor user_42Subcommands
- hookchat messages get: Show one message by its platform id
- hookchat messages human-agent: Send a human-typed message in the 24 hour to 7 day window
- hookchat messages list: List messages, newest first
- hookchat messages reply: Send a reply inside the 24 hour window
hookchat messages get#
Show one message by its platform id
hookchat messages get <id> [flags]Calls GET /v1/messages/:id. The id is the platform's own message id (mid). A message written by a test-scope key is found only with --include-test. Each attachment shows as its own line: its type and Meta's link to it. The link expires after a while, and HookChat does not keep a copy.
# One message
hookchat messages get mid_abc123
# A message sent under a test key
hookchat messages get mid_abc123 --include-test --output jsonFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--include-test | bool | include rows written by test-scope keys |
hookchat messages human-agent#
Send a human-typed message in the 24 hour to 7 day window
hookchat messages human-agent [flags]Calls POST /v1/messages/human-agent. --actor names the human who wrote the message and is required: the gateway refuses a send without it with missing_actor (exit 6). Past 7 days the gateway answers human_agent_unavailable (exit 6) and nothing can be sent. --text and --attachment follow the same rules as "messages reply": one or the other, never both, and <type>=@<file> uploads a file from your computer first.
The gateway does not send at once. It holds the message and answers with a verify_url. The person who typed the message opens it and passes a check in the browser, and HookChat then sends the message. The check raises the cost of sending by script; it is not proof that a person is there. The command prints verify_url as soon as the hold is made, and opens it in your browser when stdin and stdout are a terminal (not with --no-open). The link carries a one-time secret: give it only to the person who typed the message.
Then it waits for the send, for up to --wait-timeout (15m, as long as a hold waits; the same limit covers any upload), and prints the platform message id. With --quiet it prints the link, then the id. With --output json or yaml it prints two documents: the hold as made, with verify_url, then the hold once sent. A hold that expires is hold_expired (exit 8). A send that fails exits as the same error would on a reply: human_agent_unavailable (6), rate_limited (7), send_failed (8), conversation_not_found (4). If the wait runs out, the command exits 10 and the hold goes on until it expires.
With --no-wait it prints the hold (id, status, expires_at, verify_url) and exits 0 at once, without waiting for the send; --quiet prints only the link, and json or yaml print the one hold document. The browser rules are the same. Use it in scripts that hand the link on.
# A follow up from a named agent: opens the check in your browser
hookchat messages human-agent --conversation "CONV#acme#instagram#1#2" --text "Any update?" --actor user_42
# Print the hold and its link, and return at once
hookchat messages human-agent --conversation "CONV#acme#instagram#1#2" --text "Any update?" --actor user_42 --no-wait --no-open
# Print the link only, and wait up to five minutes
hookchat messages human-agent --conversation "CONV#acme#instagram#1#2" --text "Any update?" --actor user_42 --no-open --wait-timeout 5m
# A PDF on its own
hookchat messages human-agent --conversation "CONV#acme#instagram#1#2" --attachment file=https://example.com/quote.pdf --actor user_42
# A PDF from your computer
hookchat messages human-agent --conversation "CONV#acme#instagram#1#2" --attachment file=@./quote.pdf --actor user_42Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--actor | string | id of the human who wrote the message (required) | |
--attachment | strings | attachment as <type>=<url>, or <type>=@<file> to upload a file first; in place of --text; repeat for up to 10 images | |
--conversation | string | conversation id (required) | |
--no-open | bool | print verify_url but do not open it in a browser | |
--no-wait | bool | print the hold and exit at once, without waiting for the message to be sent | |
--reply-to | string | platform message id to reply to | |
--text | string | message text | |
--wait-timeout | duration | 15m0s | how long to wait, such as 90s or 5m. One limit covers both waits here: for Meta to have an uploaded file, and for the held message to be sent. The default here is 15m, as long as a hold waits; attachments upload and messages reply default to 2m |
hookchat messages list#
List messages, newest first
hookchat messages list [flags]Calls GET /v1/messages, the feed for the whole workspace. --conversation narrows it to one thread and --direction to inbound (from the participant), outbound (sent through the API) or echo (sent from the Instagram or Messenger app; a webhook delivers outbound and echo both as message.sent). Rows written by test-scope keys appear only with --include-test. A filtered page may be short and still carry a next_cursor.
# The last 50 messages across the workspace
hookchat messages list
# One thread, inbound only, including test rows
hookchat messages list --conversation "CONV#acme#instagram#1#2" --direction inbound --include-test
# Every message in a thread as JSON
hookchat messages list --conversation "CONV#acme#instagram#1#2" --all --output jsonFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--all | bool | walk every page instead of printing one page and its next_cursor | |
--conversation | string | restrict to one conversation id | |
--cursor | string | resume from a previous next_cursor | |
--direction | string | restrict to one direction: inbound, outbound or echo | |
--include-test | bool | include rows written by test-scope keys | |
--limit | int | 50 | page size, 1 to 100 (with --all, the page size used while walking) |
hookchat messages reply#
Send a reply inside the 24 hour window
hookchat messages reply [flags]Calls POST /v1/messages/reply. Send --text or --attachment, not both: a message carries text or media, so words and a picture take two calls. An attachment is <type>=<url> or <type>=@<file>, where type is image, video, audio or file. Meta fetches a URL, so it must be public https. The @ means a file on your computer: it is uploaded first (see "attachments upload"), the command waits for Meta to have it (up to --wait-timeout), then sends it by its attachment id. Repeat --attachment for up to 10 images in one message; a video, audio or file goes alone. Outside the 24 hour window the gateway refuses with window_closed (exit 6); check the conversation's can_send_as_human_agent flag and use "messages human-agent" when it is true.
# A text reply
hookchat messages reply --conversation "CONV#acme#instagram#1#2" --text "Thanks, on it"
# A photo from your computer
hookchat messages reply --conversation "CONV#acme#instagram#1#2" --attachment image=@./photo.jpg
# An image, replying to a specific message
hookchat messages reply --conversation "CONV#acme#instagram#1#2" --attachment image=https://example.com/a.png --reply-to mid_123
# Three images as one message
hookchat messages reply --conversation "CONV#acme#instagram#1#2" --attachment image=https://example.com/1.jpg --attachment image=https://example.com/2.jpg --attachment image=https://example.com/3.jpg
# Only the platform message id
hookchat messages reply --conversation "CONV#acme#instagram#1#2" --text "Hi" --quietFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--attachment | strings | attachment as <type>=<url>, or <type>=@<file> to upload a file first; in place of --text; repeat for up to 10 images | |
--conversation | string | conversation id (required) | |
--reply-to | string | platform message id to reply to | |
--text | string | message text | |
--wait-timeout | duration | 2m0s | how long to wait for Meta to have an uploaded file, such as 90s or 5m |
hookchat ping#
Check that the gateway is reachable
hookchat pingCalls GET /v1/ping and prints the gateway's name, version and clock. The route is unauthenticated, so ping works before any key is configured; when a key is configured it is sent anyway, which makes ping a quick check of the base URL a profile points at.
# Ping the production gateway
hookchat ping
# Ping a local gateway as JSON
hookchat ping --base-url http://localhost:3000 --output jsonhookchat realtime#
Issue websocket connect tickets
hookchat realtime [command]A browser cannot set an Authorization header on a websocket upgrade, so the realtime socket is opened with a short-lived, single-use ticket issued here and passed in the socket URL.
# Get a ticket
hookchat realtime ticket
# Just the ticket value, for a script
hookchat realtime ticket --quietSubcommands
- hookchat realtime ticket: Issue a short-lived websocket connect ticket
hookchat realtime ticket#
Issue a short-lived websocket connect ticket
hookchat realtime ticketCalls POST /v1/realtime/ticket. The ticket is single use, bound to the workspace, and expires within about a minute; open the socket before expires_at.
# Get a ticket and show its expiry
hookchat realtime ticket
# Feed the ticket to another tool
TICKET=$(hookchat realtime ticket --quiet)hookchat stats#
Show the workspace's overview counts
hookchat statsCalls GET /v1/stats: messages received, deliveries that gave up (dlq) across the workspace, and per endpoint the delivered, failed and dlq counts. Every count covers the reported window (window_hours, 24 by default), not all history.
# Overview as a table
hookchat stats
# Overview as JSON, for a dashboard
hookchat stats --output jsonhookchat verify#
Verify a captured webhook delivery offline
hookchat verify --secret <whs_...> [--file <path>] [flags]Checks a delivery's HookChat-Signature header against the signing secret and prints the event it carries. The input is a JSON document of the form {"headers": {...}, "body": "<raw body>"} with lowercased header names, which is what a capture receiver stores, read from --file or standard input. With --raw the input is the raw body itself and the headers are passed with --header "HookChat-Signature: t=...,v1=...".
The signature is HMAC-SHA256 over "<t>.<body>" with the exact body bytes; during a secret rotation the header carries two v1 values and either secret verifies. A timestamp further than --tolerance from now is rejected. A signature failure exits 9; a body that verifies but is not a HookChat event exits 1.
# A delivery captured as JSON
hookchat verify --secret whs_... --file delivery.json
# Piped from a capture receiver
curl -s http://localhost:4100/deliveries | jq '.[0]' | hookchat verify --secret whs_...
# A raw body with its signature header
hookchat verify --secret whs_... --raw --header "HookChat-Signature: t=1756900000,v1=ab12..." --file body.json
# An old fixture, ignoring the timestamp check
hookchat verify --secret whs_... --file delivery.json --tolerance 87600hFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--file | string | read the input from this file instead of stdin | |
--header | strings | a header as "Name: value", repeatable; only with --raw | |
--raw | bool | the input is the raw body; pass headers with --header | |
--secret | string | the endpoint's whs_ signing secret (required) | |
--tolerance | duration | 5m0s | maximum age of the delivery timestamp |
hookchat version#
Print the CLI version
hookchat versionPrints the hookchat CLI version. With --output json or yaml it also reports the Go toolchain, operating system and CPU architecture the binary was built for, which is useful in a bug report.
# Just the version
hookchat version
# Version plus build details
hookchat version --output jsonhookchat webhooks#
Manage webhook endpoints and their deliveries
hookchat webhooks [command]A webhook endpoint is an HTTPS URL the gateway POSTs signed events to. Create returns the whs_ signing secret exactly once; rotate issues a new one while the old one keeps signing for a 24 hour overlap. Test sends a test.event through the real delivery path, and the deliveries subcommands inspect and replay the delivery log.
# Register an endpoint for two event types
hookchat webhooks create --url https://example.com/hookchat --events message.received,message.failed
# Prove the receiver verifies signatures
hookchat webhooks test 01ARZ3NDEKTSV4RRFFQ69G5FAV
hookchat webhooks deliveries list 01ARZ3NDEKTSV4RRFFQ69G5FAVSubcommands
- hookchat webhooks create: Register an endpoint and get its signing secret
- hookchat webhooks delete: Delete an endpoint
- hookchat webhooks deliveries: Inspect and replay an endpoint's deliveries
- hookchat webhooks get: Show one endpoint
- hookchat webhooks list: List the workspace's live endpoints
- hookchat webhooks rotate: Issue a new signing secret with a 24 hour overlap
- hookchat webhooks test: Send a test.event through the real delivery path
- hookchat webhooks update: Change an endpoint's URL or events, or pause and resume it
hookchat webhooks create#
Register an endpoint and get its signing secret
hookchat webhooks create [flags]Calls POST /v1/webhooks. The URL must be HTTPS on a public host (the gateway rejects private and loopback addresses with invalid_request and the reason in detail). --events restricts the subscription; omit it for every event type. The whs_ signing secret is shown exactly once; --quiet prints only the secret.
# Every event type
hookchat webhooks create --url https://example.com/hookchat
# Only inbound messages and failures, as JSON to capture id and secret
hookchat webhooks create --url https://example.com/hookchat --events message.received,message.failed --output json
# Capture the secret directly
SECRET=$(hookchat webhooks create --url https://example.com/hookchat --quiet)Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--events | strings | event types to subscribe to, comma separated (default: all): message.received, message.sent, message.failed, test.event, account.connected, account.disconnected, participant.updated | |
--url | string | HTTPS endpoint the gateway POSTs deliveries to (required) |
hookchat webhooks delete#
Delete an endpoint
hookchat webhooks delete <id>Calls DELETE /v1/webhooks/:id. Deliveries to the endpoint stop at once, so at a terminal the command asks first; --yes skips the question, and a piped stdin never asks. The endpoint is tombstoned: its signing material is erased while its id and delivery history are kept. A second delete of the same endpoint is a no-op success.
# Delete by id
hookchat webhooks delete 01ARZ3NDEKTSV4RRFFQ69G5FAV
# Delete every endpoint pointing at a host
hookchat webhooks list --output json | jq -r '.[] | select(.url | contains("old.example.com")) | .id' | xargs -n1 hookchat webhooks delete --quiethookchat webhooks deliveries#
Inspect and replay an endpoint's deliveries
hookchat webhooks deliveries [command]Every event delivered to an endpoint is logged with its status: pending, attempting, retrying, delivered, failed, blocked or dlq. list pages the log, summary counts the failing rows by status server side, replay retries one delivery and replay-all retries every failing delivery in a time window. Delivery timestamps are epoch milliseconds in the API; tables show them as RFC 3339.
# What failed in the last 24 hours
hookchat webhooks deliveries summary 01ARZ3NDEKTSV4RRFFQ69G5FAV
# Retry everything that failed since the outage
hookchat webhooks deliveries replay-all 01ARZ3NDEKTSV4RRFFQ69G5FAV --from 6hSubcommands
- hookchat webhooks deliveries list: List an endpoint's deliveries, newest first
- hookchat webhooks deliveries replay: Retry one delivery with a fresh attempt budget
- hookchat webhooks deliveries replay-all: Retry every failing delivery in a time window
- hookchat webhooks deliveries summary: Count an endpoint's failing deliveries by status
hookchat webhooks deliveries list#
List an endpoint's deliveries, newest first
hookchat webhooks deliveries list <endpointId> [flags]Calls GET /v1/webhooks/:id/deliveries. One page is printed by default with the next_cursor on stderr (table) or in the envelope (json, yaml); --all walks every page. --status keeps only the named statuses; the filter is applied by the CLI after fetching, so a filtered page may be short. An endpoint id that does not exist in the workspace, or a deleted one, is endpoint_not_found (exit 4), as for webhooks get.
# The latest page
hookchat webhooks deliveries list 01ARZ3NDEKTSV4RRFFQ69G5FAV
# Only failed and dlq rows, across every page
hookchat webhooks deliveries list 01ARZ3NDEKTSV4RRFFQ69G5FAV --status failed,dlq --all
# Delivery ids only, for replay scripts
hookchat webhooks deliveries list 01ARZ3NDEKTSV4RRFFQ69G5FAV --status dlq --all --quietFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--all | bool | walk every page instead of printing one page and its next_cursor | |
--cursor | string | resume from a previous next_cursor | |
--limit | int | 50 | page size, 1 to 100 (with --all, the page size used while walking) |
--status | strings | keep only these statuses, comma separated (pending, attempting, retrying, delivered, failed, blocked, dlq) |
hookchat webhooks deliveries replay#
Retry one delivery with a fresh attempt budget
hookchat webhooks deliveries replay <endpointId> <deliveryId> [flags]Calls POST /v1/webhooks/:id/deliveries/:deliveryId/replay. A delivery that already succeeded is refused with replay_conflict (exit 8) unless --force is given, in which case the receiver sees a duplicate event id (dedupe on it). A delivery that is mid-attempt is always refused. The event body is kept 7 days (the delivery record 90), so a replay after that is refused with replay_expired (exit 8).
# Replay a failed delivery
hookchat webhooks deliveries replay 01ARZ3NDEKTSV4RRFFQ69G5FAV 01J8ZK3M...
# Re-send a delivered one anyway
hookchat webhooks deliveries replay 01ARZ3NDEKTSV4RRFFQ69G5FAV 01J8ZK3M... --forceFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--force | bool | replay even if the delivery already succeeded |
hookchat webhooks deliveries replay-all#
Retry every failing delivery in a time window
hookchat webhooks deliveries replay-all <endpointId> [flags]Calls POST /v1/webhooks/:id/deliveries/replay. --from is required and --to defaults to now; both accept an RFC 3339 time, a date, a unix epoch or an age such as 6h. The default statuses are dlq and failed; including delivered requires --force (exit 8 otherwise). The batch is bounded: a short page returns a next_cursor, printed on stderr in table mode. --all keeps going until the batch is exhausted and prints the summed counts.
# Everything that failed in the last 6 hours
hookchat webhooks deliveries replay-all 01ARZ3NDEKTSV4RRFFQ69G5FAV --from 6h
# A precise window, dlq only, walking every batch page
hookchat webhooks deliveries replay-all 01ARZ3NDEKTSV4RRFFQ69G5FAV --from 2026-09-03T00:00:00Z --to 2026-09-03T06:00:00Z --status dlq --all
# Re-send delivered events too (receivers see duplicate event ids)
hookchat webhooks deliveries replay-all 01ARZ3NDEKTSV4RRFFQ69G5FAV --from 1h --status delivered --forceFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--all | bool | keep replaying until the batch is exhausted | |
--cursor | string | continue a batch from a previous next_cursor | |
--force | bool | allow delivered deliveries to be re-sent | |
--from | string | start of the window: RFC 3339, a date, a unix epoch or an age such as 6h (required) | |
--limit | int | batch page size, 1 to 100 (default: server default) | |
--status | strings | statuses to replay, comma separated (default: dlq,failed; delivered needs --force) | |
--to | string | end of the window, same forms as --from (default: now) |
hookchat webhooks deliveries summary#
Count an endpoint's failing deliveries by status
hookchat webhooks deliveries summary <endpointId> [flags]Calls GET /v1/webhooks/:id/deliveries/summary. By default it covers the last 24 hours and the statuses dlq, failed, blocked and retrying. --since accepts an RFC 3339 time, a date, a unix epoch or an age such as 6h. When the examined-row cap is hit the result is marked truncated and older failures may be missing.
# Failures in the last 24 hours
hookchat webhooks deliveries summary 01ARZ3NDEKTSV4RRFFQ69G5FAV
# Only dlq rows from the last 6 hours
hookchat webhooks deliveries summary 01ARZ3NDEKTSV4RRFFQ69G5FAV --since 6h --status dlqFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--since | string | start of the window: RFC 3339, a date, a unix epoch or an age such as 6h (default: 24h) | |
--status | strings | statuses to include, comma separated (default: dlq,failed,blocked,retrying) |
hookchat webhooks get#
Show one endpoint
hookchat webhooks get <id>Calls GET /v1/webhooks/:id. A deleted endpoint, or one from another workspace, is endpoint_not_found (exit 4).
# One endpoint
hookchat webhooks get 01ARZ3NDEKTSV4RRFFQ69G5FAV
# Is a rotation overlap still active?
hookchat webhooks get 01ARZ3NDEKTSV4RRFFQ69G5FAV --output json | jq .secondary_activehookchat webhooks list#
List the workspace's live endpoints
hookchat webhooks listCalls GET /v1/webhooks. Deleted endpoints are not listed. No secret is ever returned, only a display prefix of the current one.
# Endpoints as a table
hookchat webhooks list
# Paused endpoints
hookchat webhooks list --output json | jq '.[] | select(.status == "paused")'hookchat webhooks rotate#
Issue a new signing secret with a 24 hour overlap
hookchat webhooks rotate <id>Calls POST /v1/webhooks/:id/rotate. The new whs_ secret is shown exactly once. The previous secret keeps signing for 24 hours, during which every delivery carries two v1 signatures, so a receiver holding either secret keeps verifying while you roll the new one out. At a terminal the command asks first; --yes skips the question, and a piped stdin never asks.
# Rotate and show the new secret
hookchat webhooks rotate 01ARZ3NDEKTSV4RRFFQ69G5FAV
# Capture the new secret for a secret manager
NEW=$(hookchat webhooks rotate 01ARZ3NDEKTSV4RRFFQ69G5FAV --quiet)hookchat webhooks test#
Send a test.event through the real delivery path
hookchat webhooks test <id>Calls POST /v1/webhooks/:id/test. The event is signed and delivered asynchronously like any other; poll "webhooks deliveries list" or your receiver for the delivery id printed here. --quiet prints nothing on success.
# Send a test event
hookchat webhooks test 01ARZ3NDEKTSV4RRFFQ69G5FAV
# Send one and wait for the delivery to settle
hookchat webhooks test 01ARZ3NDEKTSV4RRFFQ69G5FAV --output json | jq -r .delivery_idhookchat webhooks update#
Change an endpoint's URL or events, or pause and resume it
hookchat webhooks update <id> [flags]Calls PATCH /v1/webhooks/:id with only the fields given. --events replaces the subscription filter and --all-events clears it. --disabled pauses the endpoint: events that arrive while it is paused are not delivered and are not kept for later. Deliveries already retrying keep retrying. --enabled resumes it. At least one change is required.
# Move an endpoint to a new URL
hookchat webhooks update 01ARZ3NDEKTSV4RRFFQ69G5FAV --url https://example.com/v2/hookchat
# Pause it during a deploy, then resume
hookchat webhooks update 01ARZ3NDEKTSV4RRFFQ69G5FAV --disabled
hookchat webhooks update 01ARZ3NDEKTSV4RRFFQ69G5FAV --enabled
# Subscribe to everything again
hookchat webhooks update 01ARZ3NDEKTSV4RRFFQ69G5FAV --all-eventsFlags
| Flag | Type | Default | Description |
|---|---|---|---|
--all-events | bool | clear the event filter (subscribe to every event) | |
--disabled | bool | pause deliveries (status paused) | |
--enabled | bool | resume deliveries (status active) | |
--events | strings | new event type filter, comma separated: message.received, message.sent, message.failed, test.event, account.connected, account.disconnected, participant.updated | |
--url | string | new HTTPS endpoint URL |