Command Palette

Search for a command to run...

IntelliPortalDOCS

Start here

  • Overview
  • Quickstart
  • Test in the sandbox

Build

  • Authentication
  • Clients
  • Hosted onboarding
  • Webhooks

API reference

  • Messages
  • Usage
  • Status & observability

Reliability

  • WhatsApp limits
  • Errors & debugging

Resources

  • Changelog
  • OpenAPI 3.1
  • Postman collection
Developer support
IntelliPortalDOCS
Developer documentation/API v1

© 2026 Intelli Holdings Inc.

PrivacyContact

Need a hand?

Share a request ID when you contact us so we can trace the call quickly.

Open developer support
  1. Docs
  2. Webhooks

Core concept

HMAC-SHA256

Webhooks

Receive inbound messages and delivery lifecycle events through one signed endpoint, with channel and client_ref included for deterministic routing.

Configure one HTTPS endpoint

Save the receiver URL and event selection in the dashboard's Webhooks section. Copy the generated signing secret into your application's secret manager. Webhook configuration is currently managed through the portal, not a public /v1/webhooks route.

Configure webhooks

Event types

Subscribe only to events your product handles. New fields can be added over time, so ignore unknown properties.

eventChannelMeaning
message.receivedWhatsApp, InstagramAn end user sent a message.
message.statusWhatsAppA sent message moved to sent, delivered, read, or failed.
message.reactionWhatsApp, InstagramAn end user added or removed a reaction.
message.echoInstagramThe connected professional account sent a message.
message.readInstagramA user read a message.
message.postbackInstagramA user activated a postback action.
webhook.testAllA signed diagnostic event from the dashboard.

Route the event envelope

Use event to select a handler, channel for the provider-specific body, and client_ref to find your customer. Provider data stays close to the source payload so you can preserve message fidelity.

WhatsApp · message.received
{
  "event": "message.received",
  "channel": "whatsapp",
  "timestamp": "2026-07-24T09:42:18.000Z",
  "client_ref": "customer_42",
  "phone_number": "+254711111111",
  "contacts": [
    {
      "profile": { "name": "Amina" },
      "wa_id": "254700000000"
    }
  ],
  "messages": [
    {
      "from": "254700000000",
      "id": "wamid.xxx",
      "type": "text",
      "text": { "body": "Is my order ready?" }
    }
  ]
}
Instagram · message.received
{
  "event": "message.received",
  "channel": "instagram",
  "timestamp": "2026-07-24T09:44:01.000Z",
  "client_ref": "customer_42_instagram",
  "contact": {
    "id": "17841400000000000",
    "username": "ama_mensah",
    "name": "Ama Mensah"
  },
  "messages": [
    {
      "id": "mid.xxx",
      "sender": {
        "id": "17841400000000000",
        "username": "ama_mensah",
        "name": "Ama Mensah"
      },
      "recipient": { "id": "17841499999999999" },
      "text": "Can you help?"
    }
  ]
}

Downloading media a customer sent

The two channels hand you inbound media differently.

WhatsApp gives an id only — messages[].image.id and friends. Fetching the file needs the client's Meta credentials, which you never hold, so request it from us: GET /v1/media/<media_id>?client_ref=<ref> with your API key (clients:read scope). It streams the bytes back with the real Content-Type, plus Content-Length and an X-Media-Sha256 you can verify against the webhook. Meta keeps inbound media for 30 days.

Instagram needs no call: the payload already carries a fetchable URL at attachments[].payload.url. It is short-lived, so download on receipt rather than when someone opens the conversation.

Instagram users arrive with their handle

Instagram identifies people by an Instagram-scoped ID, which is opaque and account-specific. We resolve it for you: every participant that isn't your client's own business account carries username and name, and the person the event is about is repeated at the top level as contact. Both are omitted when Instagram has no profile to return, so treat them as optional and keep keying your records on the ID.

Store provider message IDs

Use the message or status ID as your deduplication key. Automatic retries and manual redelivery can cause the same event to arrive more than once.

Verify the signature before parsing

Read X-Intelli-Signature, compute HMAC-SHA256 over the exact raw request bytes, prefix the hex digest with sha256=, and compare in constant time.

Signature verification
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyIntelliSignature(
  rawBody: Buffer,
  signature: string,
  secret: string,
) {
  const expected =
    "sha256=" +
    createHmac("sha256", secret).update(rawBody).digest("hex");

  const received = Buffer.from(signature);
  const calculated = Buffer.from(expected);

  return (
    received.length === calculated.length &&
    timingSafeEqual(received, calculated)
  );
}

Do not re-serialize JSON

Parsing and stringifying the body changes whitespace or key order and invalidates the HMAC. Preserve the raw bytes before any JSON middleware consumes them.

Acknowledge, then process

Verify, persist or enqueue, and return a 2xx response within five seconds. Move database fan-out, AI work, outbound replies, and third party calls into an asynchronous worker.

  1. 1

    Immediate

  2. 2

    10 seconds

  3. 3

    60 seconds

  4. 4

    5 minutes

Non-2xx responses and timeouts are retried on this schedule. After the final attempt, the delivery remains available in logs for inspection and manual redelivery.

Test and inspect deliveries

  • Use Send test event to check reachability, HTTP status, and signature handling.
  • Use the Simulator to send realistic signed payloads through the delivery pipeline.
  • Open Delivery Logs to inspect the full payload, response, attempts, and client.
  • Use Resend after fixing the receiver; expect your deduplication layer to see the same event.
Open simulatorOpen delivery logs
PreviousHosted onboarding
NextMessages