Skip to content

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 │ │ │
└──────────────────┘ └────────────────────┘ └────────────────┘
  1. 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
    }'
    FieldRequiredNotes
    allowed_originsyesWho may frame the page. Feeds the page’s frame-ancestors and the postMessage targetOrigin. Up to 10.
    platformsnowhatsapp (default), instagram, facebook.
    external_refnoYour tenant id. Comes back verbatim in the webhook and the result.
    ttl_minutesno30 by default, 1440 max.
    prefillnoBusiness 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 token is returned once. Hold it only as long as it takes to render the iframe: the session dies when it completes or expires.

  2. <iframe
    src="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>
  3. 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_origins are honored — the same list that is allowed to frame the page.

  4. typeWhenPayload
    neike:readyThe page loadedplatforms
    neike:resizeContent height changedheight in px
    neike:channel_connectedThe channel is livechannel
    neike:errorA step failedmessage
    window.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);
    });
  5. When the channel connects we POST to {your_webhook_url}/channels/connected with your x-webhook-token header:

    {
    "event": "channel.connected",
    "session_id": "nkes_9fQ2…",
    "external_ref": "tenant_8842",
    "channel": { "…": "the channel payload" }
    }
  6. Terminal window
    curl https://api.neike.dev/api/embed/sessions/nkes_9fQ2… \
    -H "x-neike-api-key: whk_live_…"

    status is pending, opened, completed, revoked or expired. When it is completed, result holds 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.

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