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#

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.

TypeScript
npm install @hookchat/node
Python
pip install hookchat
Go
go get github.com/jiffi-co/jiffi-message-gateway/packages/hookchat-go@v0.1.1
CLI
curl -fsSL https://raw.githubusercontent.com/jiffi-co/jiffi-message-gateway/main/packages/hookchat-cli/install.sh | bash

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

Check the install
hookchat version

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

Shell
# 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"
Shell
hookchat auth status

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

TypeScript
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)
The secret is shown once, at creation, and never again. Put it in a secret manager or an environment variable such as 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.

TypeScript
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))
Keep the signing secret next to your receiver as 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.

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

TypeScript
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:

A test.event delivery
{
  "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.

TypeScript
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)
Outside the 24 hour window the reply route refuses with 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.

Next#