Embedded signup
If you are building a product where your customers connect their own WhatsApp numbers, you do not want to send them to our console. Embed the flow in yours instead.
Your customer never creates a Meta app, never verifies a domain and never sees a token. Meta validates a single origin — ours — and every one of your customers reuses it, so onboarding customer number 500 needs exactly as much Meta configuration as customer number 1: none.
your app Neike Meta ┌──────────────────┐ ┌────────────────────┐ ┌────────────────┐ │ backend │──(1)──▶│ POST /api/embed/… │ │ │ │ │◀───────│ embed_url + token │ │ │ │ frontend │ │ │ │ │ │ <iframe src=…> │──(2)──▶│ GET /embed/signup │──(3)─▶│ Embedded Signup│ │ │◀─(5)───│ postMessage │◀──────│ code + WABA │ │ backend │◀─(4)───│ channel.connected │ │ │ └──────────────────┘ └────────────────────┘ └────────────────┘-
Create a session from your backend
Section titled “Create a session from your backend”A session is single use and authorises exactly one channel connection. Your backend creates it with your API key, so the token is never minted in a browser.
Terminal window curl -X POST https://api.neike.dev/api/embed/sessions \-H "x-neike-api-key: whk_live_…" \-H "Content-Type: application/json" \-d '{"allowed_origins": ["https://app.yourcompany.com"],"platforms": ["whatsapp"],"external_ref": "tenant_8842","ttl_minutes": 30}'Field Required Notes allowed_originsyes Who may frame the page. Feeds the page’s frame-ancestorsand the postMessagetargetOrigin. Up to 10.platformsno whatsapp(default),instagram,facebook.external_refno Your tenant id. Comes back verbatim in the webhook and the result. ttl_minutesno 30 by default, 1440 max. prefillno Business details you already know, passed to Meta so the form arrives filled in. {"session_id": "nkes_9fQ2…","token": "nkst_…","embed_url": "https://api.neike.dev/embed/signup?token=nkst_…","expires_at": "2026-08-09T18:30:00.000Z","status": "pending"}The
tokenis returned once. Hold it only as long as it takes to render the iframe: the session dies when it completes or expires. -
Frame it
Section titled “Frame it”<iframesrc="https://api.neike.dev/embed/signup?token=nkst_…"width="100%"height="720"style="border:0;border-radius:12px"sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms"></iframe> -
Match your theme
Section titled “Match your theme”By default the frame follows the operating system preference. That is fine if your app does the same — and jarring if it doesn’t. A user running Windows in dark mode inside your light-themed app would get a black rectangle in the middle of your page.
Tell us instead. Passed in the URL, it applies on the first paint, with no flash:
<iframe src="https://api.neike.dev/embed/signup?token=nkst_…&theme=light"></iframe>themeResult system(default)Follows the OS prefers-color-scheme.lightAlways light, even on a dark OS. darkAlways dark, even on a light OS. Anything else falls back to
system, so a typo in?theme=never breaks the frame.If your app has a theme switch, tell the frame when the user flips it and it follows along without reloading:
iframe.contentWindow.postMessage({ source: "neike-host", type: "neike:theme", theme: "dark" },"https://api.neike.dev",);Only messages coming from one of the session’s
allowed_originsare honored — the same list that is allowed to frame the page. -
Listen in the frontend
Section titled “Listen in the frontend”typeWhen Payload neike:readyThe page loaded platformsneike:resizeContent height changed heightin pxneike:channel_connectedThe channel is live channelneike:errorA step failed messagewindow.addEventListener("message", (event) => {if (event.origin !== "https://api.neike.dev") return;const msg = event.data;if (msg?.source !== "neike-embed") return;if (msg.type === "neike:resize") iframe.style.height = msg.height + "px";if (msg.type === "neike:channel_connected") onChannelReady(msg.channel);}); -
Receive the webhook
Section titled “Receive the webhook”When the channel connects we
POSTto{your_webhook_url}/channels/connectedwith yourx-webhook-tokenheader:{"event": "channel.connected","session_id": "nkes_9fQ2…","external_ref": "tenant_8842","channel": { "…": "the channel payload" }} -
Or read the session back
Section titled “Or read the session back”Terminal window curl https://api.neike.dev/api/embed/sessions/nkes_9fQ2… \-H "x-neike-api-key: whk_live_…"statusispending,opened,completed,revokedorexpired. When it iscompleted,resultholds the channel payload.
The channel payload
Section titled “The channel payload”This is everything your app needs to operate the channel on its own — send messages, build templates and show it in your UI:
{ "channel_id": "nk_a1b2c3d4e5f6", "platform": "whatsapp", "status": "active", "display_name": "Aurora Store", "phone_number": "+54 9 11 5555-5555", "platform_channel_id": "123456789012345", "waba_id": "987654321098765", "message_template_namespace": "a1b2c3d4_e5f6_…", "verified_name": "Aurora Store", "quality_rating": "GREEN", "code_verification_status": "VERIFIED", "account_review_status": "APPROVED", "currency": "ARS", "external_ref": "tenant_8842", "connected_at": "2026-08-09T18:12:44.310Z"}channel_id is what you use against the Neike API. waba_id plus
message_template_namespace are what you need for templates.
Security model
Section titled “Security model”- Single-use token, stored only as a SHA-256 hash. It authorises one channel for the client that issued it, until it completes or expires.
- Per-session
frame-ancestors: the page can only be framed from the origins you declared. - The browser never sees Neike or Meta credentials. The WABA token is encrypted (AES-256-GCM) and stays server side.
- The channel being worked on is always read from the session, never from the request body — holding the token does not let anyone touch another channel.
- Revoke at any time:
POST /api/embed/sessions/{id}/revoke.