REST · v1

API documentation

Use the WhatsApp, Instagram, Facebook and Google channels connected to Nekna360 from your own software: send messages, read conversations and receive events instantly via webhooks. NeknaCRM and NeknaAI use this same API.

Getting started

All requests take and return JSON. Base URL:

https://nekna360.com/api/v1
  1. Create an API key on the Developers page of the panel and pick the permissions you need.
  2. Connect at least one channel on the Channels page (e.g. WhatsApp).
  3. 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

FieldDescription
channels:readAccount info (/me), channel list and channel status (incl. QR code)
channels:writeCreate, re-pair and disconnect WhatsApp QR channels
contacts:readList contacts
contacts:writeCreate/update contacts, bulk import
messages:readRead conversations and messages
messages:sendSend messages
conversations:writeConversation status, assignment, mark as read
webhooks:manageManage 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" }
HTTPerrorMeaning
401missing_api_key / invalid_api_keyNo key, wrong key or revoked
403insufficient_scopeThe key lacks the permission (see the required field)
403api_not_in_plan / organization_inactiveNo API in your plan, or the account is suspended
403message_quota_reached / contact_limit_reachedPlan limit reached
403qr_channel_limit_reached / channel_limit_reachedWhatsApp QR account or total channel limit reached (number in limit)
404conversation_not_found, channel_not_found…Not found, or belongs to another organization
409channel_not_connectedThe channel is not connected
409outside_24h_windowOfficial WhatsApp API: more than 24 h passed, send a template
409contact_do_not_contactThe contact is on the do-not-contact list
422invalid_bodyInvalid body; details in issues
429rate_limitedToo 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

GET/mechannels:read

The 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"] }
}
GET/channelschannels:read

Connected 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" }
] }
POST/channels/whatsapp-qrchannels:write

Creates 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, … } }
GET/channels/{id}channels:read

Returns 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" }
} }
POST/channels/{id}/connectchannels:write

Restarts pairing for a disconnected or logged-out QR channel (conversation history is kept).

DELETE/channels/{id}channels:write

Logs the WhatsApp QR channel's linked device out. Conversations are kept; pair the same channel again with /connect.

Contacts

GET/contacts?q=&limit=100&cursor=contacts:read

Search (q: name, phone, e-mail) and cursor listing.

POST/contactscontacts:write

Creates 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 }'
POST/contacts/bulkcontacts:write

Create/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

POST/messagesmessages:send

The message is queued and you get 202. The result arrives via the message.sent / message.failed webhooks; you can also poll GET /messages/{id}.

FieldTypeDescription
conversation_idstringReply in an existing conversation (any channel type).
channel_id + tostringStart or continue a WhatsApp chat by phone number. The contact is created if missing. WhatsApp channels only.
namestring?Name of the contact if a new one is created.
textstring?Up to 4000 characters.
mediaobject?{ type: image|video|audio|document, data: base64, mimetype, filename } — up to 16 MB.
templateobject?Official WhatsApp API template: { name, language, params[] }.
client_refstring?Your own id. Sending again with the same value returns the first message instead of a new one (duplicate: true).
WhatsApp — text
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" }'
Reply in a conversation — document
{ "conversation_id": "cmvq…",
  "text": "Teklif ektedir.",
  "media": { "type": "document", "data": "<base64>", "mimetype": "application/pdf", "filename": "teklif.pdf" } }
Official WhatsApp API — template
{ "channel_id": "cmvc…", "to": "+905321234567",
  "template": { "name": "randevu_hatirlatma", "language": "tr", "params": ["Ayşe", "28 Ekim 14:00"] } }
202 Accepted
{ "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).

GET/messages/{id}messages:read

Conversations

GET/conversationsmessages:read

Sorted 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 }
GET/conversations/{id}messages:read
PATCH/conversations/{id}conversations:write

Changes the status or assigns an agent (assignee_id: a member id from /me, null to unassign).

{ "status": "closed" }        { "assignee_id": "cmu…" }
GET/conversations/{id}/messages?limit=50&before=messages:read

Newest first. For older ones pass before=<oldest message id>. Internal notes with include_notes=true.

POST/conversations/{id}/readconversations:write

Clears 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).

EventWhen
message.receivedA new message from a customer.
message.sentA message reached the channel (from the panel, the API, a campaign or typed on the phone).
message.failedA message could not be sent; reason in message.error.
message.statusDelivery/read status changed (delivered, read).
conversation.updatedA conversation was closed, reopened, set pending or assigned.
channel.statusA channel's connection changed (e.g. the WhatsApp session was logged out).
POST https://your-app.example.com/webhooks/nekna360 — message.received
{
  "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

FieldTypeDescription
x-n360-eventstringEvent name
x-n360-deliverystringEvent id (evt_…). Stays the same on retries; use it to drop duplicates.
x-n360-attemptnumberAttempt number
x-n360-timestampunix sTime of the signature
x-n360-signaturehexHMAC-SHA256(secret, timestamp + "." + body)
GET/webhookswebhooks:manage
POST/webhookswebhooks:manage
{ "url": "https://crm.example.com/webhooks/nekna360", "events": ["message.received", "message.status"], "description": "NeknaCRM" }
→ 201 { "data": { "id": "cmvq…", …, "secret": "whsec_…" } }
PATCH/webhooks/{id}webhooks:manage
{ "events": ["*"] }      { "active": false }
DELETE/webhooks/{id}webhooks:manage
POST/webhooks/{id}/testwebhooks:manage

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

Node.js (Express)
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
});
PHP
$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);
Python
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.

  1. 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.
  2. Open the channel with POST /api/v1/channels/whatsapp-qr and show the QR from GET /api/v1/channels/{id} in your own screen.
  3. Add a webhook (POST /api/v1/webhooks), show messages in your UI and send replies with POST /api/v1/messages.
  4. 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_…
POST/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.

GET/organizations · /organizations/{ref}
PATCH/organizations/{ref}
{ "status": "active", "plan": "kurumsal", "limits": { "qr_channels": 5 } }
POST/organizations/{ref}/keys
{ "revoke_others": true }   → 201 { "api_key": "n360_…" }
POST/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.

Questions? Contact us.