Getting started
All requests take and return JSON. Base URL:
https://nekna360.com/api/v1- Create an API key on the Developers page of the panel and pick the permissions you need.
- Connect at least one channel on the Channels page (e.g. WhatsApp).
- To receive events, add a webhook URL on the same page.
To create keys and webhooks, open the Developers page in the panel.
Authentication
Send the key with every request in the Authorization header. Keys are shown once when created; keep them on your server, never in a browser or mobile app.
curl https://nekna360.com/api/v1/me \
-H "Authorization: Bearer n360_xxxxxxxxxxxxxxxx"Permissions
| Field | Description | |
|---|---|---|
| channels:read | Account info (/me), channel list and channel status (incl. QR code) | |
| channels:write | Create, re-pair and disconnect WhatsApp QR channels | |
| contacts:read | List contacts | |
| contacts:write | Create/update contacts, bulk import | |
| messages:read | Read conversations and messages | |
| messages:send | Send messages | |
| conversations:write | Conversation status, assignment, mark as read | |
| webhooks:manage | Manage webhook endpoints |
Errors and limits
Errors come with an HTTP status and a machine-readable error field. Each key may make 300 requests per minute; above that you get 429 with a retry-after header.
HTTP/1.1 403 Forbidden
{ "error": "insufficient_scope", "required": "messages:send" }| HTTP | error | Meaning |
|---|---|---|
| 401 | missing_api_key / invalid_api_key | No key, wrong key or revoked |
| 403 | insufficient_scope | The key lacks the permission (see the required field) |
| 403 | api_not_in_plan / organization_inactive | No API in your plan, or the account is suspended |
| 403 | message_quota_reached / contact_limit_reached | Plan limit reached |
| 403 | qr_channel_limit_reached / channel_limit_reached | WhatsApp QR account or total channel limit reached (number in limit) |
| 404 | conversation_not_found, channel_not_found… | Not found, or belongs to another organization |
| 409 | channel_not_connected | The channel is not connected |
| 409 | outside_24h_window | Official WhatsApp API: more than 24 h passed, send a template |
| 409 | contact_do_not_contact | The contact is on the do-not-contact list |
| 422 | invalid_body | Invalid body; details in issues |
| 429 | rate_limited | Too many requests; wait retry-after seconds |
Pagination
List endpoints use cursors. Send the next_cursor value (next_before for messages) with the next request; null means you're on the last page.
GET https://nekna360.com/api/v1/conversations?limit=50
→ { "data": [ … ], "next_cursor": "cmv…" }
GET https://nekna360.com/api/v1/conversations?limit=50&cursor=cmv…Account and channels
/mechannels:readThe organization behind the key, its members (user ids for assignment) and the key's permissions.
{
"organization": { "id": "cmv…", "name": "Atlas Yapı", "status": "ACTIVE" },
"members": [ { "id": "cmu…", "name": "Can Demir", "email": "can@atlas.com", "role": "agent" } ],
"key": { "name": "NeknaCRM", "scopes": ["messages:read", "messages:send"] }
}/channelschannels:readConnected channels. Use the id as channel_id when sending.
{ "data": [
{ "id": "cmvp…", "type": "WHATSAPP_QR", "name": "WhatsApp +905321234567",
"status": "connected", "phone": "+905321234567", "connected_at": "2026-10-27T09:12:00.000Z" }
] }/channels/whatsapp-qrchannels:writeCreates a new WhatsApp QR channel and starts pairing. If the plan's WhatsApp QR account limit (qr_channels) is reached you get 403 qr_channel_limit_reached. Disconnected channels don't count.
curl -X POST https://nekna360.com/api/v1/channels/whatsapp-qr \
-H "Authorization: Bearer n360_…" -H "Content-Type: application/json" -d '{ "name": "Satış hattı" }'
→ 201 { "data": { "id": "cmvq…", "type": "WHATSAPP_QR", "status": "pending", "qr": null, … } }/channels/{id}channels:readReturns the channel status. While pairing, qr holds the current code: value (for your own renderer) and a ready svg. The code changes about every 20 seconds; poll every 3–5 seconds while you show it. Once status is "connected", qr becomes null and phone is filled. A channel.status webhook is sent too.
{ "data": {
"id": "cmvq…", "type": "WHATSAPP_QR", "name": "Satış hattı", "status": "pending", "phone": null,
"qr": { "value": "2@Xk3…", "svg": "<svg xmlns=…>…</svg>", "generated_at": "2026-10-27T10:02:11.000Z" }
} }/channels/{id}/connectchannels:writeRestarts pairing for a disconnected or logged-out QR channel (conversation history is kept).
/channels/{id}channels:writeLogs the WhatsApp QR channel's linked device out. Conversations are kept; pair the same channel again with /connect.
Contacts
/contacts?q=&limit=100&cursor=contacts:readSearch (q: name, phone, e-mail) and cursor listing.
/contactscontacts:writeCreates or updates by phone. opt_in: true records marketing consent (for campaigns).
curl -X POST https://nekna360.com/api/v1/contacts \
-H "Authorization: Bearer n360_…" -H "Content-Type: application/json" \
-d '{ "name": "Ayşe Yılmaz", "phone": "05321234567", "tags": ["web"], "opt_in": true }'/contacts/bulkcontacts:writeCreate/update up to 1000 contacts per request (by phone). opt_in is set per contact; the ids array in the response follows input order (null = skipped).
{ "contacts": [ { "name": "Ayşe", "phone": "05321234567", "opt_in": true }, { "name": "Mert", "phone": "05339876543" } ], "tags": ["fuar-2026"] }
→ { "created": 2, "updated": 0, "skipped": 0, "invalid": 0, "ids": ["cmv…", "cmv…"] }Sending messages
/messagesmessages:sendThe message is queued and you get 202. The result arrives via the message.sent / message.failed webhooks; you can also poll GET /messages/{id}.
| Field | Type | Description |
|---|---|---|
| conversation_id | string | Reply in an existing conversation (any channel type). |
| channel_id + to | string | Start or continue a WhatsApp chat by phone number. The contact is created if missing. WhatsApp channels only. |
| name | string? | Name of the contact if a new one is created. |
| text | string? | Up to 4000 characters. |
| media | object? | { type: image|video|audio|document, data: base64, mimetype, filename } — up to 16 MB. |
| template | object? | Official WhatsApp API template: { name, language, params[] }. |
| client_ref | string? | Your own id. Sending again with the same value returns the first message instead of a new one (duplicate: true). |
curl -X POST https://nekna360.com/api/v1/messages \
-H "Authorization: Bearer n360_…" -H "Content-Type: application/json" \
-d '{ "channel_id": "cmvp…", "to": "05321234567", "name": "Ayşe Yılmaz",
"text": "Merhaba Ayşe Hanım, teklifiniz hazır.", "client_ref": "crm-offer-8812" }'{ "conversation_id": "cmvq…",
"text": "Teklif ektedir.",
"media": { "type": "document", "data": "<base64>", "mimetype": "application/pdf", "filename": "teklif.pdf" } }{ "channel_id": "cmvc…", "to": "+905321234567",
"template": { "name": "randevu_hatirlatma", "language": "tr", "params": ["Ayşe", "28 Ekim 14:00"] } }{ "data": {
"id": "cmvq0jw9o…", "conversation_id": "cmvpyt25…", "channel_id": "cmvp…", "channel_type": "WHATSAPP_QR",
"contact_id": "cmvpw239…", "direction": "out", "type": "text", "text": "Merhaba Ayşe Hanım, teklifiniz hazır.",
"media": null, "status": "pending", "error": null, "external_id": null, "source": "api",
"client_ref": "crm-offer-8812", "created_at": "2026-10-27T09:30:11.388Z"
} }Message statuses: pending → sent → delivered → read, or failed (reason in error).
/messages/{id}messages:readConversations
/conversationsmessages:readSorted by latest activity. Filters: status (open|pending|closed), channel_id, contact_id, phone, updated_after (ISO date).
GET https://nekna360.com/api/v1/conversations?status=open&phone=05321234567
{ "data": [ {
"id": "cmvq…", "channel_id": "cmvp…", "channel_type": "WHATSAPP_QR", "channel_name": "WhatsApp +9053…",
"contact": { "id": "cmvp…", "name": "Ayşe Yılmaz", "phone": "+905321234567", "email": null },
"status": "open", "assignee_id": null, "unread_count": 2,
"last_message_at": "2026-10-27T09:31:02.000Z", "last_preview": "Fiyat alabilir miyim?",
"last_inbound_at": "2026-10-27T09:31:02.000Z", "created_at": "2026-10-20T08:00:00.000Z"
} ], "next_cursor": null }/conversations/{id}messages:read/conversations/{id}conversations:writeChanges the status or assigns an agent (assignee_id: a member id from /me, null to unassign).
{ "status": "closed" } { "assignee_id": "cmu…" }/conversations/{id}/messages?limit=50&before=messages:readNewest first. For older ones pass before=<oldest message id>. Internal notes with include_notes=true.
/conversations/{id}/readconversations:writeClears the unread count and sends read receipts (blue ticks on WhatsApp).
Webhooks
Once you add a webhook, Nekna360 sends a signed POST request to your URL for the events you selected. The URL must be https on a public server. Any non-2xx response counts as a failure and is retried 8 times with growing gaps over about an hour; after 50 consecutive undelivered events the webhook is paused (resume it in the panel).
| Event | When | |
|---|---|---|
| message.received | A new message from a customer. | |
| message.sent | A message reached the channel (from the panel, the API, a campaign or typed on the phone). | |
| message.failed | A message could not be sent; reason in message.error. | |
| message.status | Delivery/read status changed (delivered, read). | |
| conversation.updated | A conversation was closed, reopened, set pending or assigned. | |
| channel.status | A channel's connection changed (e.g. the WhatsApp session was logged out). |
{
"id": "evt_4f0c2b0e9a6c4c8f9d3a1b7e5c2d8f10",
"type": "message.received",
"created_at": "2026-10-27T09:31:02.412Z",
"org_id": "cmvpuinuk…",
"data": {
"message": {
"id": "cmvq…", "conversation_id": "cmvq…", "channel_id": "cmvp…", "channel_type": "WHATSAPP_QR",
"contact_id": "cmvp…", "direction": "in", "type": "text", "text": "Fiyat alabilir miyim?",
"media": null, "status": "received", "external_id": "3EB0…", "source": "customer", "created_at": "2026-10-27T09:31:02.000Z"
},
"contact": { "id": "cmvp…", "name": "Ayşe Yılmaz", "phone": "+905321234567" }
}
}Headers
| Field | Type | Description |
|---|---|---|
| x-n360-event | string | Event name |
| x-n360-delivery | string | Event id (evt_…). Stays the same on retries; use it to drop duplicates. |
| x-n360-attempt | number | Attempt number |
| x-n360-timestamp | unix s | Time of the signature |
| x-n360-signature | hex | HMAC-SHA256(secret, timestamp + "." + body) |
/webhookswebhooks:manage/webhookswebhooks:manage{ "url": "https://crm.example.com/webhooks/nekna360", "events": ["message.received", "message.status"], "description": "NeknaCRM" }
→ 201 { "data": { "id": "cmvq…", …, "secret": "whsec_…" } }/webhooks/{id}webhooks:manage{ "events": ["*"] } { "active": false }/webhooks/{id}webhooks:manage/webhooks/{id}/testwebhooks:manageVerifying signatures
Sign the raw body (before parsing JSON) and compare with the header in constant time; reject timestamps older than 5 minutes. The secret (whsec_…) is shown once when the webhook is created.
import crypto from "node:crypto";
app.post("/webhooks/nekna360", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("x-n360-timestamp");
const sig = req.get("x-n360-signature") || "";
const expected = crypto.createHmac("sha256", process.env.N360_WEBHOOK_SECRET)
.update(ts + "." + req.body).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (!fresh || sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(200); // answer first…
queue.add(event); // …then process
});$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_N360_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_N360_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $body, getenv('N360_WEBHOOK_SECRET'));
if (abs(time() - (int) $ts) > 300 || !hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
http_response_code(200);import hmac, hashlib, time
def verify(raw_body: bytes, ts: str, sig: str, secret: str) -> bool:
if abs(time.time() - int(ts)) > 300:
return False
expected = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)Partner API
Nekna products (NeknaCRM, NeknaAI) and partners create and manage Nekna360 organizations for their own customers. The platform key (np_…) is issued by the Nekna team and used under /api/partner/v1. When an organization is created, a full-access API key (n360_…) for it is returned once; that customer's messages, conversations and channels are then handled through /api/v1 with that key.
- When a customer turns WhatsApp on in your product, create the organization with POST /organizations (ref = your customer id). Store the returned api_key encrypted for that customer.
- Open the channel with POST /api/v1/channels/whatsapp-qr and show the QR from GET /api/v1/channels/{id} in your own screen.
- Add a webhook (POST /api/v1/webhooks), show messages in your UI and send replies with POST /api/v1/messages.
- If the customer will also use the Nekna360 panel, pass owner_email to get a one-time invite link.
https://nekna360.com/api/partner/v1
Authorization: Bearer np_…/organizations{ "ref": "crm-tenant-42", "name": "Atlas Yapı", "plan": "profesyonel", "trial_days": 14,
"limits": { "qr_channels": 2, "monthly_messages": 5000 }, "owner_email": "ali@atlas.com" }
→ 201 { "data": { "id": "cmv…", "ref": "crm-tenant-42", "status": "trial", "plan": { "code": "profesyonel", … },
"limits": { "channels": 10, "qr_channels": 2, … }, "usage": { "channels": 0, "qr_channels": 0 }, … },
"created": true, "api_key": "n360_…", "invite_url": "https://nekna360.com/davet/…" }limits overrides plan limits per customer: channels, qr_channels, seats, contacts, monthly_messages, monthly_ai_credits. -1 = unlimited, null = the plan's value.
/organizations · /organizations/{ref}/organizations/{ref}{ "status": "active", "plan": "kurumsal", "limits": { "qr_channels": 5 } }/organizations/{ref}/keys{ "revoke_others": true } → 201 { "api_key": "n360_…" }/organizations/{ref}/invites{ "email": "ali@atlas.com", "role": "owner" } → 201 { "invite_url": "https://nekna360.com/davet/…", "expires_in_days": 7 }Rules and best practices
- The WhatsApp QR connection is not the official API: it is for one-to-one chats. Don't send bulk or unsolicited messages; the number may get restricted. Use an official WhatsApp Business API channel for notifications and campaigns.
- On the official WhatsApp API, only approved templates can be sent 24 hours after the customer's last message.
- Give every message a client_ref (e.g. the record id in your CRM) so it is never sent twice.
- Answer webhooks with 200 quickly and do the work in the background; a request without a response in 10 seconds counts as failed.
- Events can occasionally repeat or arrive out of order: dedupe with x-n360-delivery, order with created_at.
- API messages count against your plan's monthly message quota, the same counter as the panel.