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.

FlagTypeDefaultDescription
--api-keystringAPI key (hookchat_live_... or hookchat_test_...); overrides HOOKCHAT_API_KEY and the profile
--base-urlstringAPI origin; overrides HOOKCHAT_BASE_URL and the profile (default https://hookchat.dev)
--no-colorbooldisable colour even when stdout is a terminal
-o, --outputstringtableoutput format: table, json or yaml
--profilestringconfig profile to use; overrides HOOKCHAT_PROFILE (default: the config's default profile, else "default")
-q, --quietboolprint only ids or values, nothing else on success
--tenantstringworkspace slug, required for an operator key; overrides HOOKCHAT_TENANT and the profile
--timeoutduration30sper-request timeout
-y, --yesboolanswer yes to the question before revoke, delete, rotate and erase

Commands#

hookchat#

The HookChat command line: Instagram and Messenger DMs as webhooks

Usage
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.

Exit codes
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)
Examples
# 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

Usage
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.

Examples
# 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

Usage
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.

Examples
# 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 --yes

hookchat accounts list#

List linked accounts and their credential health

Usage
hookchat accounts list

Calls 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.

Examples
# 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

Usage
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".

Examples
# 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_01J9Z8Y7X6W5V4T3S2R1Q0P9N8

Subcommands

hookchat attachments get#

Show where an upload has got to

Usage
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.

Examples
# 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 json

hookchat attachments upload#

Upload a file and print its attachment id

Usage
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).

Examples
# 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 5m

Flags

FlagTypeDefaultDescription
--content-typestringthe media type, such as image/jpeg (default: from the file name)
--conversationstringconversation id the file will be sent in (required)
--filestringpath of the file to upload (required)
--filenamestringthe name Meta sees (default: the file's own name)
--typestringhow the file will be sent: image, video, audio or file (default: from the content type)
--wait-timeoutduration2m0show long to wait for Meta to have an uploaded file, such as 90s or 5m

hookchat audit#

Read the workspace's audit trail

Usage
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.

Examples
# The most recent 50 entries
hookchat audit list

# Everything, as JSON
hookchat audit list --all --output json

Subcommands

hookchat audit list#

List audit entries, most recent first

Usage
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.

Examples
# One page of 20
hookchat audit list --limit 20

# Continue from a cursor
hookchat audit list --cursor eyJ...

# Every entry
hookchat audit list --all

Flags

FlagTypeDefaultDescription
--allboolwalk every page instead of printing one page and its next_cursor
--cursorstringresume from a previous next_cursor
--limitint50page size, 1 to 100 (with --all, the page size used while walking)

hookchat auth#

Log in, inspect and remove credential profiles

Usage
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.

Examples
# 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 ping

Subcommands

hookchat auth login#

Validate an API key and store it as a profile

Usage
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.

Examples
# 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-default

Flags

FlagTypeDefaultDescription
--set-defaultboolmake this profile the default profile

hookchat auth logout#

Remove a profile from the config file

Usage
hookchat auth logout

Deletes 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.

Examples
# Remove the default profile
hookchat auth logout

# Remove a named profile
hookchat auth logout --profile acme

hookchat auth status#

Show the active profile and ping the gateway

Usage
hookchat auth status

Shows 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.

Examples
# Human readable status
hookchat auth status

# Machine readable, including the ping result
hookchat auth status --output json

hookchat completion#

Print a shell completion script

Usage
hookchat completion bash|zsh|fish|powershell

Prints a completion script for the named shell. Load it in your shell's startup file to complete commands and flags:

Output
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
Examples
# Bash, for the current session
source <(hookchat completion bash)

# Zsh, installed permanently
hookchat completion zsh > "${fpath[1]}/_hookchat"

hookchat conversations#

Read the inbox

Usage
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.

Examples
# 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

Usage
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.

Examples
# 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 json

hookchat conversations get#

Show one conversation and its recent messages

Usage
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.

Examples
# 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.state

Flags

FlagTypeDefaultDescription
--allboolwalk every page instead of printing one page and its next_cursor
--cursorstringresume from a previous next_cursor
--include-testboolinclude rows written by test-scope keys
--limitint50page size, 1 to 100 (with --all, the page size used while walking)

hookchat conversations list#

List conversations, most recently active first

Usage
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.

Examples
# 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

FlagTypeDefaultDescription
--allboolwalk every page instead of printing one page and its next_cursor
--cursorstringresume from a previous next_cursor
--limitint50page size, 1 to 100 (with --all, the page size used while walking)

hookchat docs#

Generate and check the command reference

Usage
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.

Examples
# 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 check

Subcommands

hookchat docs check#

Exit 1 when the committed reference is stale

Usage
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".

Examples
# Check the committed file
hookchat docs check

# Check another path
hookchat docs check --file build/reference.md

Flags

FlagTypeDefaultDescription
--filestringdocs/cli/reference.mdthe reference file to compare

hookchat docs generate#

Write the Markdown command reference

Usage
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.

Examples
# Write the committed reference
hookchat docs generate --out docs/cli/reference.md

# Preview on stdout
hookchat docs generate --out -

Flags

FlagTypeDefaultDescription
--outstringdocs/cli/reference.mdoutput path, or - for stdout

hookchat keys#

Create, list and revoke API keys

Usage
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.

Examples
# Create a test key for CI
hookchat keys create --label ci --scope test

# Revoke it later
hookchat keys revoke 01ARZ3NDEKTSV4RRFFQ69G5FAV

Subcommands

hookchat keys create#

Create a new API key

Usage
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.

Examples
# 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

FlagTypeDefaultDescription
--labelstringdisplay label (default "key for <tenant>")
--scopestringkey scope: live or test (default: the caller's scope)

hookchat keys list#

List the workspace's API keys

Usage
hookchat keys list

Calls GET /v1/keys. Only the display prefix of each key is returned, never a secret. Revoked keys stay in the list with status revoked.

Examples
# 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

Usage
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).

Examples
# 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 --quiet

hookchat messages#

Read the message feed and send replies

Usage
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).

Examples
# 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_42

Subcommands

hookchat messages get#

Show one message by its platform id

Usage
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.

Examples
# One message
hookchat messages get mid_abc123

# A message sent under a test key
hookchat messages get mid_abc123 --include-test --output json

Flags

FlagTypeDefaultDescription
--include-testboolinclude rows written by test-scope keys

hookchat messages human-agent#

Send a human-typed message in the 24 hour to 7 day window

Usage
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.

Examples
# 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_42

Flags

FlagTypeDefaultDescription
--actorstringid of the human who wrote the message (required)
--attachmentstringsattachment as <type>=<url>, or <type>=@<file> to upload a file first; in place of --text; repeat for up to 10 images
--conversationstringconversation id (required)
--no-openboolprint verify_url but do not open it in a browser
--no-waitboolprint the hold and exit at once, without waiting for the message to be sent
--reply-tostringplatform message id to reply to
--textstringmessage text
--wait-timeoutduration15m0show 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

Usage
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.

Examples
# 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 json

Flags

FlagTypeDefaultDescription
--allboolwalk every page instead of printing one page and its next_cursor
--conversationstringrestrict to one conversation id
--cursorstringresume from a previous next_cursor
--directionstringrestrict to one direction: inbound, outbound or echo
--include-testboolinclude rows written by test-scope keys
--limitint50page size, 1 to 100 (with --all, the page size used while walking)

hookchat messages reply#

Send a reply inside the 24 hour window

Usage
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.

Examples
# 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" --quiet

Flags

FlagTypeDefaultDescription
--attachmentstringsattachment as <type>=<url>, or <type>=@<file> to upload a file first; in place of --text; repeat for up to 10 images
--conversationstringconversation id (required)
--reply-tostringplatform message id to reply to
--textstringmessage text
--wait-timeoutduration2m0show long to wait for Meta to have an uploaded file, such as 90s or 5m

hookchat ping#

Check that the gateway is reachable

Usage
hookchat ping

Calls 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.

Examples
# Ping the production gateway
hookchat ping

# Ping a local gateway as JSON
hookchat ping --base-url http://localhost:3000 --output json

hookchat realtime#

Issue websocket connect tickets

Usage
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.

Examples
# Get a ticket
hookchat realtime ticket

# Just the ticket value, for a script
hookchat realtime ticket --quiet

Subcommands

hookchat realtime ticket#

Issue a short-lived websocket connect ticket

Usage
hookchat realtime ticket

Calls 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.

Examples
# 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

Usage
hookchat stats

Calls 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.

Examples
# Overview as a table
hookchat stats

# Overview as JSON, for a dashboard
hookchat stats --output json

hookchat verify#

Verify a captured webhook delivery offline

Usage
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.

Examples
# 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 87600h

Flags

FlagTypeDefaultDescription
--filestringread the input from this file instead of stdin
--headerstringsa header as "Name: value", repeatable; only with --raw
--rawboolthe input is the raw body; pass headers with --header
--secretstringthe endpoint's whs_ signing secret (required)
--toleranceduration5m0smaximum age of the delivery timestamp

hookchat version#

Print the CLI version

Usage
hookchat version

Prints 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.

Examples
# Just the version
hookchat version

# Version plus build details
hookchat version --output json

hookchat webhooks#

Manage webhook endpoints and their deliveries

Usage
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.

Examples
# 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 01ARZ3NDEKTSV4RRFFQ69G5FAV

Subcommands

hookchat webhooks create#

Register an endpoint and get its signing secret

Usage
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.

Examples
# 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

FlagTypeDefaultDescription
--eventsstringsevent types to subscribe to, comma separated (default: all): message.received, message.sent, message.failed, test.event, account.connected, account.disconnected, participant.updated
--urlstringHTTPS endpoint the gateway POSTs deliveries to (required)

hookchat webhooks delete#

Delete an endpoint

Usage
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.

Examples
# 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 --quiet

hookchat webhooks deliveries#

Inspect and replay an endpoint's deliveries

Usage
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.

Examples
# 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 6h

Subcommands

hookchat webhooks deliveries list#

List an endpoint's deliveries, newest first

Usage
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.

Examples
# 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 --quiet

Flags

FlagTypeDefaultDescription
--allboolwalk every page instead of printing one page and its next_cursor
--cursorstringresume from a previous next_cursor
--limitint50page size, 1 to 100 (with --all, the page size used while walking)
--statusstringskeep only these statuses, comma separated (pending, attempting, retrying, delivered, failed, blocked, dlq)

hookchat webhooks deliveries replay#

Retry one delivery with a fresh attempt budget

Usage
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).

Examples
# Replay a failed delivery
hookchat webhooks deliveries replay 01ARZ3NDEKTSV4RRFFQ69G5FAV 01J8ZK3M...

# Re-send a delivered one anyway
hookchat webhooks deliveries replay 01ARZ3NDEKTSV4RRFFQ69G5FAV 01J8ZK3M... --force

Flags

FlagTypeDefaultDescription
--forceboolreplay even if the delivery already succeeded

hookchat webhooks deliveries replay-all#

Retry every failing delivery in a time window

Usage
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.

Examples
# 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 --force

Flags

FlagTypeDefaultDescription
--allboolkeep replaying until the batch is exhausted
--cursorstringcontinue a batch from a previous next_cursor
--forceboolallow delivered deliveries to be re-sent
--fromstringstart of the window: RFC 3339, a date, a unix epoch or an age such as 6h (required)
--limitintbatch page size, 1 to 100 (default: server default)
--statusstringsstatuses to replay, comma separated (default: dlq,failed; delivered needs --force)
--tostringend of the window, same forms as --from (default: now)

hookchat webhooks deliveries summary#

Count an endpoint's failing deliveries by status

Usage
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.

Examples
# 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 dlq

Flags

FlagTypeDefaultDescription
--sincestringstart of the window: RFC 3339, a date, a unix epoch or an age such as 6h (default: 24h)
--statusstringsstatuses to include, comma separated (default: dlq,failed,blocked,retrying)

hookchat webhooks get#

Show one endpoint

Usage
hookchat webhooks get <id>

Calls GET /v1/webhooks/:id. A deleted endpoint, or one from another workspace, is endpoint_not_found (exit 4).

Examples
# One endpoint
hookchat webhooks get 01ARZ3NDEKTSV4RRFFQ69G5FAV

# Is a rotation overlap still active?
hookchat webhooks get 01ARZ3NDEKTSV4RRFFQ69G5FAV --output json | jq .secondary_active

hookchat webhooks list#

List the workspace's live endpoints

Usage
hookchat webhooks list

Calls GET /v1/webhooks. Deleted endpoints are not listed. No secret is ever returned, only a display prefix of the current one.

Examples
# 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

Usage
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.

Examples
# 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

Usage
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.

Examples
# 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_id

hookchat webhooks update#

Change an endpoint's URL or events, or pause and resume it

Usage
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.

Examples
# 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-events

Flags

FlagTypeDefaultDescription
--all-eventsboolclear the event filter (subscribe to every event)
--disabledboolpause deliveries (status paused)
--enabledboolresume deliveries (status active)
--eventsstringsnew event type filter, comma separated: message.received, message.sent, message.failed, test.event, account.connected, account.disconnected, participant.updated
--urlstringnew HTTPS endpoint URL