Platforms and resellers
Run a website builder, a bills app, a shop platform or any service for other businesses? Give each of them their own WhatsApp assistant without building one. You create a workspace for each client, they link their number by scanning a QR code, and we answer their customers. You pay per reply from one wallet.
Everything below can also be done by hand from your dashboard under WhatsApp → Platform, so you can see each step before you code it.
Use a test key (io_test_sk_…) for this whole section. It costs nothing and touches no real number. Get a key in API Keys. All requests go to https://api.identifyorg.com with the header X-IdentifyOrg-Key.
1. Create a workspace
curl -X POST https://api.identifyorg.com/v1/wa/channels \
-H "X-IdentifyOrg-Key: io_test_sk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"label": "Ada Stores",
"external_id": "client_8841",
"ai_enabled": true,
"brand": { "name": "YourPlatform", "color": "#0a7f3f", "logo_url": "https://yourplatform.com/logo.png" },
"profile": {
"business_name": "Ada Stores",
"about": "We sell phones and accessories in Yaba, Lagos.",
"services": "Phones, chargers, repairs, airtime.",
"hours": "Mon to Sat, 9am to 6pm",
"tone": "friendly",
"language": "English"
}
}'HTTP/1.1 201 Created
{
"id": "5b1f3c1e-7a42-4d0e-9a2c-1f0c7a1d9e33",
"label": "Ada Stores",
"business_name": "Ada Stores",
"status": "new",
"ai_enabled": true,
"profile_ready": true,
"external_id": "client_8841",
"test": true,
"brand": { "name": "YourPlatform", "color": "#0a7f3f", "logo_url": "https://yourplatform.com/logo.png" },
"connect_url": "https://identifyorg.com/connect/Qm4k…",
"phone": null,
"msgs_in": 0,
"created_at": "2026-10-14T09:12:03Z"
}2. Start linking
POST /v1/wa/channels/{id}/connect
{
"status": "pairing",
"qr": "identifyorg-test-qr:Qm4k…", // null on live workspaces: fetch it with GET (below)
"connect_url": "https://identifyorg.com/connect/Qm4k…",
"test": true
}3. Pretend the QR code was scanned
curl -X POST https://api.identifyorg.com/v1/wa/channels/$ID/test/scan \
-H "X-IdentifyOrg-Key: io_test_sk_xxxxxxxx"
{ "status": "connected", "phone": "2340000000000", "test": true }4. Send a pretend customer message
curl -X POST https://api.identifyorg.com/v1/wa/channels/$ID/test/inbound \
-H "X-IdentifyOrg-Key: io_test_sk_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "text": "How much is a Samsung A15?", "from_phone": "2348012345678", "name": "Tola" }'
{
"replies": [ { "type": "text", "text": "The Samsung A15 is ₦185,000. Want me to hold one for you?" } ],
"conversation_id": "a77d90c2-…",
"needs_human": false,
"charged": 0,
"test": true
}That is the whole loop. Swap the test key for a live key when you are ready (see Go live).
POST /connect returns a placeholder QR string. POST /test/scan makes the workspace connected, exactly as a real scan would, and sends the whatsapp.connected webhook.POST /test/inbound sends a pretend customer message through the real pipeline: your keyword rules, your own system (signed the same way as live, so you can test your endpoint and signature check) and the assistant. You get back the replies that would have been sent. Nothing goes to WhatsApp.GET /usage using a test key.A workspace is one client business with one WhatsApp number, its own information, rules, conversations and usage. Nobody can see another account's workspaces.
POST /v1/wa/channels
| Field | Type | Required | What it does |
|---|---|---|---|
| label | string | Yes | A name for the workspace. Usually the client's business name. Up to 120 characters. |
| external_id | string | No | Your own id for this client. Letters, numbers, - _ . : Must be unique among your workspaces. Use it to look the workspace up: GET /v1/wa/channels?external_id=… |
| ai_enabled | boolean | No | Default true. false turns the AI off: rules and your own system still answer, at the lower per-reply price. |
| mode | string | No | ai (default): the assistant answers. site: your own system answers everything that no rule handles. |
| profile | object | No | The business information. See Behaviour for every field. |
| brand | object | No | name, logo_url (https), color (#RRGGBB). Shown on the hosted connect page. |
GET /v1/wa/channels lists your workspaces. Add ?external_id=client_8841 to find one.GET /v1/wa/channels/{id} returns one workspace with live status and the QR code while pairing.PATCH /v1/wa/channels/{id} changes label, ai_enabled, mode or brand.POST /v1/wa/channels/{id}/disconnect unlinks the number. DELETE /v1/wa/channels/{id} removes the workspace and its conversations.The client opens WhatsApp on the phone that holds the business number, goes to Linked devices, and scans a QR code. You can show that QR code in two ways.
Option A: the hosted connect page (nothing to build)
Every workspace has a connect_url. Redirect your client to it, or put it in a frame. It shows your name, logo and colour, the QR code (refreshed automatically), the steps, and a success message when linked. Nothing on it says IdentifyOrg unless you leave the brand empty.
<iframe
src="https://identifyorg.com/connect/Qm4k…"
style="width:100%;max-width:420px;height:560px;border:0;border-radius:16px"
allow="clipboard-write"></iframe>Option B: draw the QR code yourself
Call POST /connect, then poll GET /channels/{id} every 2 seconds. While status is pairing the response has a qr string. Draw it with any QR library. It changes about every 20 seconds, so always draw the latest one. Stop polling when status becomes connected, or listen for the whatsapp.connected webhook.
GET /v1/wa/channels/{id} // poll every 2 seconds while status is "pairing"
{
"id": "5b1f3c1e-…",
"status": "pairing", // new → pairing → connected (also: disconnected, logged_out)
"qr": "2@Vh3…", // the text to draw as a QR code. Present only while status is "pairing".
"phone": null, // filled in once connected
...
}A typical integration
import { IdentifyOrgClient } from "@identifyorg/node-sdk"; // or plain fetch, see the cURL examples
const API = "https://api.identifyorg.com";
const headers = { "X-IdentifyOrg-Key": process.env.IDENTIFYORG_KEY, "Content-Type": "application/json" };
const call = (path, method = "GET", body) =>
fetch(API + path, { method, headers, body: body ? JSON.stringify(body) : undefined }).then(async (r) => {
const data = await r.json();
if (!r.ok) throw new Error(data.error?.message ?? r.statusText);
return data;
});
// 1. when one of your clients turns on WhatsApp
const ws = await call("/v1/wa/channels", "POST", {
label: client.businessName,
external_id: client.id,
profile: { business_name: client.businessName, about: client.about, services: client.services },
});
// 2. start linking and send the client to the hosted page (or draw the QR yourself)
const { connect_url } = await call(`/v1/wa/channels/${ws.id}/connect`, "POST");
redirect(connect_url);
// 3. wait for the webhook "whatsapp.connected", then mark the client as live in your own databaseThe connect page shows your name, logo and colour. Set them once for your whole account, and override any of them for a single workspace. The page picks, in this order: the workspace's own value, then your account default, then your account name with a plain colour.
PUT /v1/wa/brand (set once for every workspace you have)
{ "name": "YourPlatform", "color": "#0a7f3f", "logo_url": "https://yourplatform.com/logo.png" }
→ { "name": "YourPlatform", "color": "#0a7f3f", "logo_url": "https://yourplatform.com/logo.png" }
POST /v1/wa/brand/logo multipart/form-data, field "file" (PNG, JPEG, WebP or SVG, up to 5 MB)
→ { "name": "YourPlatform", "color": "#0a7f3f", "logo_url": "https://api.identifyorg.com/media/partner-logos/9f2c….png" }
PATCH /v1/wa/channels/{id} { "brand": { "color": "#aa0000" } } // this workspace only; empty fields fall back to your default
POST /v1/wa/channels/{id}/brand/logo multipart, field "file" // a logo for this workspace onlyname: up to 60 characters. color: #RRGGBB. logo_url: an https address, or use the upload endpoints and we host it for you.GET /v1/wa/brand returns your default, and DELETE /v1/wa/brand clears it.You and your clients control how each assistant behaves with the business information, keyword rules, trained knowledge and the AI switch. You do not manage prompts or models.
PUT /v1/wa/channels/{id}/profile
{
"business_name": "Ada Stores", // required
"about": "…", "services": "…", "pricing": "…", "hours": "…", "location": "…", "contact": "…",
"website": "https://…", "extra": "anything else the assistant should know",
"tone": "friendly", // friendly | professional | playful, or your own words
"language": "English", // the default language of replies
"greeting": "Hi! How can I help?", // used on a customer's first message
"handoff_message": "A person from the team will reply here shortly.",
"faqs": [ { "q": "Do you deliver?", "a": "Yes, within Lagos, same day." } ]
}PUT /v1/wa/channels/{id}/rules
PUT /v1/wa/channels/{id}/rules
{ "rules": [
{ "words": ["hours", "open"], "match": "contains", "action": "reply", "reply": "We are open Monday to Saturday, 9am to 6pm." },
{ "words": ["order"], "match": "starts", "action": "site" }
] }action is reply (send the fixed text), guide (send the text, then your system handles the next messages), site (hand the message to your system) or ai (let the assistant answer). match is exact, starts or contains. Up to 30 rules.
Teach the assistant more with POST /channels/{id}/knowledge/text, /knowledge/website and /knowledge/file. Turn the AI on or off with PATCH and ai_enabled. To see how it answers before customers do, use POST /channels/{id}/test with { "message": "…" }.
Your platform knows things the assistant cannot, such as a client's orders or a customer's bill. Give each workspace a web address and we send it customer messages as signed requests. Your system answers in the response, or later through POST /channels/{id}/send. Use a rule with action site to choose which messages go there.
PUT /v1/wa/channels/{id}/website
{ "url": "https://yourplatform.com/api/whatsapp/client_8841",
"fallback_message": "Sorry, we are having trouble right now. A person will reply soon." }
→ { "endpoint_url": "…", "endpoint_secret": "whsec_…", "mode": "ai", ... }The exact message format, the signature check and the reply format are in the WhatsApp API guide. You can test the whole thing with test/inbound in test mode.
Choose pay per reply and there is no monthly fee. Every answered customer message is taken from your wallet:
| Reply | Price | When |
|---|---|---|
| With the AI on | ₦10 | ai_enabled is true. One price covers the reply and the conversation. |
| With the AI off | ₦5 | ai_enabled is false. Rules and your own system still answer. |
whatsapp.balance_low before and whatsapp.balance_empty and whatsapp.paused after.GET /v1/wa/plans always returns the current ones.GET /v1/wa/plans
{ "plans": [ … monthly plans … ],
"pay_per_reply": { "ai_reply": 10.0, "plain_reply": 5.0, "grace_replies": 20 },
"trial": { "days": 7, "numbers": 1, "messages": 100 } }
POST /v1/wa/subscription { "tier": "payg" }
{ "tier": "payg", "name": "Pay per reply", "status": "active", "active": true,
"pay_per_reply": { "ai_reply": 10.0, "plain_reply": 5.0, "grace_replies": 20 },
"balance": 12500.0 }You decide what your own clients pay. We bill you; you bill them however you like, using the per-workspace usage below.
GET /v1/wa/usage?days=30 (optional: &channel_id=…)
{
"days": 30, "currency": "NGN",
"summary": { "replies": 1840, "ai_replies": 1500, "plain_replies": 340, "charged": 16700.0 },
"by_channel": [
{ "channel_id": "5b1f…", "label": "Ada Stores", "external_id": "client_8841",
"replies": 920, "ai_replies": 800, "plain_replies": 120, "charged": 8600.0 }
],
"by_day": [ { "date": "2026-10-13", "replies": 61, "charged": 560.0 } ]
}Add &mode=test or &mode=live when using a dashboard login. API keys see their own mode automatically.
Add an address in Webhooks and choose these events. Every event is signed; see Webhooks for how to check the signature. You can also resend any event from the log and send sample events to your address.
| Event | When | data |
|---|---|---|
| whatsapp.connected | A client scanned the QR code and the number is linked. | channel_id, external_id, label, phone, test |
| whatsapp.disconnected | The number was unlinked, logged out on the phone, or the connection was lost. | …plus reason: disconnected_by_you | logged_out_on_phone | connection_lost |
| whatsapp.paused | Customers wrote but nothing answered because the wallet ran out (or the plan ended). Sent at most once a day per workspace. | …plus reason |
| whatsapp.balance_low | Fewer than 50 replies left in your wallet. Sent at most once a day. | balance, replies_left, price_per_reply |
| whatsapp.balance_empty | The wallet ran dry. Replies continue for a short grace, then pause. | balance, grace_replies, message |
| whatsapp.message.received | A customer message arrived (also delivered to your website if you connected one). | see the WhatsApp API guide |
POST https://yourplatform.com/webhooks/identifyorg
X-IdentifyOrg-Signature: t=1791650000,v1=9f2b…c41a
{
"id": "6c7e…",
"event": "whatsapp.connected",
"created_at": "2026-10-14T09:15:40Z",
"data": {
"channel_id": "5b1f3c1e-…",
"external_id": "client_8841",
"label": "Ada Stores",
"phone": "2348012345678",
"test": false
}
}io_live_sk_…).POST /v1/wa/subscription with { "tier": "payg" }, or press the button in WhatsApp → Platform.POST /v1/wa/channels call.connect_url. They scan with the phone that holds the business number.whatsapp.connected webhook. They are live.Errors look like { "error": { "code": "duplicate_external_id", "message": "…", "status": 409 } }.
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or invalid API key. |
| 404 | not_found | The workspace does not exist, or it belongs to the other mode. A test key never sees live workspaces and a live key never sees test ones. |
| 409 | duplicate_external_id | You already have a workspace in this mode with that external_id. The message includes its id. |
| 409 | not_pairing | You called test/scan before starting linking with POST /connect. |
| 400 | not_a_test_workspace | test/scan and test/inbound only work on test workspaces. |
| 403 | test_limit | You can keep 25 test workspaces. Delete some you no longer need. |
| 403 | channel_limit | Your plan allows fewer live numbers than you tried to create. |
| 402 | insufficient_balance | Your wallet is too low for the action, such as starting a monthly plan. |
| 429 | rate_limited | Too many test messages in an hour (120 per hour), or too many requests to the connect page. |
| 400 | profile_too_long | The business information is over the size limit (14,000 characters in total). |
whatsapp.disconnected and they can scan again from the same connect_url.Finding this hard to integrate?
Open a ticket and our technical team will help you get it working. We reply by email. Or WhatsApp or call +234 913 995 7618.