Skip to content

Core concepts

Your client is your account: one webhook URL, one shared token, one wallet, one team. Everything you connect belongs to it.

A channel is one connected messaging endpoint — a WhatsApp number, an Instagram professional account or a Facebook page. Each one gets a stable id with an nk_ prefix:

nk_dvzobn7m4h4y

That id is what you pass to every API call and what comes back in every webhook. The underlying Meta identifiers (phone number id, WABA id, IGSID, page id) are also exposed, but you never need them to operate the channel.

Meta access tokens are encrypted at rest with AES-256-GCM and never leave the server. Your integration never handles a Meta token.

Inbound messages are normalised before they reach you, so your handler does not branch per platform. Every dispatch has the same two top-level keys:

{
"event": "message.received",
"channel": { "id": "nk_dvzobn7m4h4y", "platform": "whatsapp" },
"contact": {
"id": "5491112345678",
"name": "Ana",
"phone": "+5491112345678",
"username": null,
"avatar_url": null
},
"message": {
"id": "wamid.HBgNNTQ5MTEy…",
"direction": "inbound",
"timestamp": "2026-08-09T18:12:44.310Z",
"type": "text",
"content": { "text": "Do you have stock?" },
"reply_to": null
}
}

event tells you what happened, so a single handler can switch on it.

contact.id is the counterpart’s identifier — the phone number on WhatsApp, the IGSID on Instagram, the PSID on Messenger. Together with channel.id it is the key of a conversation on your side. The envelope is self-contained: the contact travels inline, so rendering “who wrote and what they said” never needs a second request.

message.type decides the shape of message.content, and the type is never repeated inside it. Text is { text }; attachments are { url, mime_type, media_id, caption? }; a reaction is { emoji, reacted_message_id }; a location is { latitude, longitude, name?, address?, map_url }. reply_to holds the quoted message id when the customer replied to something.

Nothing is delivered synchronously, in either direction.

Inbound. Meta calls us, we verify the HMAC signature, store media in object storage, deduplicate, normalise, and put the result on a queue. A worker POSTs it to your backend. If your endpoint fails or times out, the job is retried; the outcome is recorded in an event store, so a failed dispatch can be replayed from the console instead of being lost.

Outbound. POST /api/outbound/send answers 202 and a jobId. That means queued, not delivered. A worker resolves the channel, checks your wallet, decrypts the token and calls Meta. The real outcome comes back to you as a status webhook.

You configure one base URL. We append the path:

WhatPath
WhatsApp inbound{base}/chat/whatsapp/receive
Instagram inbound{base}/chat/instagram/receive
Messenger inbound{base}/chat/facebook/receive
Status updates{base}/chat/whatsapp/update

Every request carries your shared token in the x-webhook-token header. Compare it before processing — it is what proves the call came from us.

API requests authenticate with an API key:

x-neike-api-key: whk_live_…

Keys are whk_live_ plus 40 characters, stored only as a SHA-256 hash, and shown once at creation. They can be rotated with a grace period so you migrate without downtime, and revoked instantly.

Each key carries scopesmessages:send, messages:read, templates:manage, channels:manage, webhooks:manage, usage:read, billing:manage, team:manage, keys:manage, marketing:read, marketing:manage. An endpoint declares what it needs; a key without that scope gets 403. marketing:manage is never granted by default — it spends the advertiser’s money, so you tick it on purpose (see Marketing).

Console sessions authenticate differently: a bearer JWT plus x-neike-client-id when the user belongs to more than one client.

Outbound messages draw from a prepaid wallet, priced per Meta’s conversation categories. With insufficient balance a send is rejected with 402 — inbound messages keep flowing regardless, so you never lose a customer message because of a balance.