Getting started
HookChat turns Instagram and Messenger DMs into signed webhooks and gives you two send routes back. This page takes you from install to a first reply: one key, one endpoint, one test event, one message.
Before you start#
- A HookChat workspace with a linked Instagram or Messenger account. Sign in to the console to create one.
- Node 20 or newer for TypeScript, Python 3.10 or newer, or Go 1.26 or newer. The CLI needs nothing else.
- Somewhere public to receive HTTPS requests. For local work a tunnel is fine; the gateway rejects private and loopback URLs.
1. Install#
Every SDK is one typed method per API operation plus a webhook verifier. Pick one, or install the CLI on its own; every step below shows all four.
npm install @hookchat/nodepip install hookchatgo get github.com/jiffi-co/jiffi-message-gateway/packages/hookchat-go@v0.1.1curl -fsSL https://raw.githubusercontent.com/jiffi-co/jiffi-message-gateway/main/packages/hookchat-cli/install.sh | bashThe install script downloads the release binary for your OS and CPU, checks its SHA-256 and puts hookchat on your path. With a Go toolchain you can use go install github.com/jiffi-co/jiffi-message-gateway/packages/hookchat-cli/cmd/hookchat@v0.1.1 instead. Release binaries for every platform are on the downloads page.
hookchat version2. Sign in to the CLI#
The CLI stores a key, a base URL and a tenant in a named profile so later commands need no flags. Paste the key at the prompt, or pass it with --api-key. The command pings the gateway before it writes anything.
# Paste the key at the prompt, or pass --api-key. Add --base-url when you run your own gateway
hookchat auth login --profile docs --api-key "$HOOKCHAT_API_KEY" --base-url "$HOOKCHAT_BASE_URL" --tenant "$HOOKCHAT_TENANT"hookchat auth statusFlags override environment variables (HOOKCHAT_API_KEY, HOOKCHAT_BASE_URL, HOOKCHAT_TENANT), which override the profile. auth status shows which source is active. The full command list is in the CLI reference.
3. Create an API key#
Keys are bound to your workspace and carry a scope. A hookchat_live_ key sends real messages; a hookchat_test_ key routes sends through a mock connection and writes rows that reads only show with include_test. Mint your first key in the console, then mint the rest from code.
import { HookChat } from '@hookchat/node'
const client = new HookChat({ apiKey: process.env.HOOKCHAT_API_KEY!, baseUrl: process.env.HOOKCHAT_BASE_URL! })
// The plaintext secret is returned exactly once. Store it now; no read path returns it again.
const created = await client.keys.create({ scope: 'test', label: 'docs' })
console.log('created', created.id, created.prefix)HOOKCHAT_API_KEY; never in source control.4. Register a webhook endpoint#
An endpoint is a public HTTPS URL the gateway POSTs signed events to. Creating one mints its signing secret (whs_), returned exactly once. Omit events to subscribe to every event type.
import { HookChat } from '@hookchat/node'
const client = new HookChat({ apiKey: process.env.HOOKCHAT_API_KEY!, baseUrl: process.env.HOOKCHAT_BASE_URL! })
const { endpoint, signing_secret } = await client.webhooks.create({ url: process.env.WEBHOOK_URL! })
console.log('endpoint', endpoint.id, endpoint.status)
console.log('signing secret starts with', signing_secret.slice(0, 4))WEBHOOK_SECRET. The endpoint id is what you pass to every later webhook command. A private, loopback or plain HTTP URL is refused with invalid_request and the reason in detail.5. Receive your first event#
Every delivery is HMAC-SHA256 signed over the exact bytes sent, with the signature in the HookChat-Signature header. The SDK verifier checks the signature and a 300 second timestamp tolerance, then returns the typed event. Verify the raw body; re-serialising the JSON changes the bytes.
import { createServer } from 'node:http'
import { verifyWebhook, HookChatSignatureError } from '@hookchat/node'
const secret = process.env.WEBHOOK_SECRET!
createServer((req, res) => {
const chunks: Buffer[] = []
req.on('data', (chunk: Buffer) => chunks.push(chunk))
req.on('end', () => {
const raw = Buffer.concat(chunks) // the exact bytes, never re-serialised
try {
const event = verifyWebhook(raw, req.headers, secret)
console.log('verified', event.type, event.id)
res.writeHead(200).end()
} catch (error) {
if (error instanceof HookChatSignatureError) {
res.writeHead(401).end()
return
}
throw error
}
})
}).listen(Number(process.env.PORT))With the receiver running, send a test.event through the real signing and delivery path. The gateway returns a delivery id straight away and delivers asynchronously.
import { HookChat } from '@hookchat/node'
const client = new HookChat({ apiKey: process.env.HOOKCHAT_API_KEY!, baseUrl: process.env.HOOKCHAT_BASE_URL! })
const { delivery_id } = await client.webhooks.test(process.env.WEBHOOK_ID!)
console.log('queued test delivery', delivery_id)Your receiver logs verified test.event evt_test_.... The body it verified looks like this:
{
"id": "evt_test_5f1c9a2b7d3e",
"type": "test.event",
"created": 1756900000,
"data": {
"test": {
"message": "HookChat test event",
"tenant": "acme",
"nonce": "5f1c9a2b7d3e"
}
}
}6. Send your first reply#
A real inbound DM arrives as message.received with a conversation_id. Reply inside the 24 hour window that opens with the participant's last message. The conversation id is the whole address: there is no tenant parameter on the send routes.
import { HookChat } from '@hookchat/node'
const client = new HookChat({ apiKey: process.env.HOOKCHAT_API_KEY!, baseUrl: process.env.HOOKCHAT_BASE_URL! })
const { platform_message_id } = await client.messages.reply({
conversation_id: process.env.CONVERSATION_ID!,
text: 'Thanks for the message, on it now.',
})
console.log('sent', platform_message_id)window_closed (409). From 24 hours to 7 days a named human can still follow up on the human-agent route; past 7 days nothing can be sent. See Reply within the window and Send as a human agent.