Documentation

Integrate Znatoka

Add contact-safe buyer↔seller dispute chat to your platform over a REST API + HMAC webhook. Fastest path: hand the spec to your AI coder below.

For AI coders

Hand this to Cursor, Claude Code, or Windsurf

Paste one URL and your AI coder fetches the entire integration spec — endpoints, webhook, HMAC, SDK. No PDF, no login.

Full spec (LLM-ready)
OpenAPI 3.1 (JSON)
Ready-to-paste prompt
You are integrating Znatoka (contact-safe buyer<->seller dispute chat) into my app.

Read the full API + webhook spec here: https://znatoka.com/llms.txt
OpenAPI 3.1 JSON: https://znatoka.com/openapi.json
Official SDK: npm package "chatdisiniaja" (Node/TypeScript).

Requirements:
- Keep CHATDISINIAJA_API_KEY server-side only (never expose to the browser).
- When a dispute/refund starts, POST /api/v1/conversations to get an id plus
  buyer_url/seller_url (or the ready buyer_embed/seller_embed <script> snippets).
- Deliver the correct link/snippet to each party.
- Webhook is OPTIONAL: the embed/SSE chat works without it. Add one only to notify
  a party who isn't viewing the chat — verify the X-Chatdisiniaja-Signature header
  (HMAC-SHA256 over the raw body) and notify notify_role via open_conversation_url.
  Otherwise rely on the embed widget or poll GET /api/v1/conversations/:id.
- Close the conversation when the dispute is resolved.

Follow the spec exactly. Do not invent endpoints or fields.
Get your api_key & webhook_secret by signing up free. Manage them anytime in the Portal.

Easiest way — embed widget

No chat UI to build. Create a conversation (step 1) to get a token for each party, then paste this single lineon your platform's page — a floating chat bubble appears (like Tawk.to), with no contact leaking:

<script src="https://znatoka.com/embed.js"
        data-token="<this_party_token>" async></script>

The token is in buyer_url/seller_url (the part after /c/), or use the buyer_embed/seller_embedfield from the create response directly — it's already a ready-to-paste snippet. Options: data-position="left|right", data-color="#5b7cfa", data-label="Help", data-fullscreen="mobile|always|off" (default mobile — opens full-screen on phones so nothing looks cramped).

Support widget + unread badge

embed.jsabove needs a token per party — it suits buyer↔seller disputes you create from your backend. For plain visitor → support chat on your own site, use widget.js instead: paste one line on every page, no backend call, no token to manage.

<script src="https://znatoka.com/widget.js"
        data-site="<your widget_key>" async></script>

Your widget_key (wk_...) is public — it is safe in page source, unlike your api_key. The widget gives each visitor a random id in localStorage and keeps one open conversation per visitor, so a returning visitor lands back in the same thread. Read and reply to everything in the admin inbox.

Unread badge

When you reply and the visitor has the panel closed, a red count appears on the bubble — no setup needed, it is on by default. It works because the widget loads the chat hidden as soon as the page does, so its realtime stream is already connected before anyone clicks the bubble. The count clears the moment the visitor opens the panel, and is remembered per browser, so a page reload will not re-announce messages they already read.

If the tab is in the background, the page title also blinks (1) Pesan baru until the visitor comes back. It never blinks while they are looking at the page, and the original title is always restored. Turn it off with data-title-alert="off".

The bubble badge and the title blink are the only signals we can raise on your page. Sound and desktop notifications exist inside the chat itself, but browsers block both from a cross-origin frame, so do not rely on them reaching a visitor who has the panel closed.

Options

<script src="https://znatoka.com/widget.js"
        data-site="wk_..."
        data-label="Help"            <!-- bubble tooltip + screen-reader name -->
        data-position="left"         <!-- left | right (default right) -->
        data-color="#5b7cfa"         <!-- bubble gradient start -->
        data-fullscreen="mobile"     <!-- mobile | always | off -->
        data-title-alert="on"        <!-- on | off: blink the tab title -->
        async></script>

Everything renders in a shadow root, so your CSS and the widget's cannot affect each other. data-fullscreen="mobile" (the default) opens full-screen on phones so nothing looks cramped. The bubble is a real <button> whose accessible name carries the unread count, and the count is announced politely to screen readers.

Official SDK (Node/TypeScript)

The chatdisiniaja package wraps the REST API + HMAC webhook verification + embed helpers, fully typed.

npm install chatdisiniaja

import { Chatdisiniaja } from 'chatdisiniaja';
const cdj = new Chatdisiniaja({ apiKey: process.env.CHATDISINIAJA_API_KEY! });
const conv = await cdj.createConversation({
  externalRef: 'REFUND-123',
  buyer: { name: 'Buyer #4821' },
  seller: { name: 'Seller (anonymous)' },
});
// paste conv.buyer_embed / conv.seller_embed on each party's page

Authentication

All /api/v1/* endpoints require the header X-Api-Key: <api_key>. Participant endpoints /c/:token/* are opened directly by the buyer/seller via a secret link — no API key. Keep your api key server-side only.

Endpoints

MethodPathDescription
POST/api/v1/conversationsCreate a conversation and return buyer_url & seller_url.
GET/api/v1/conversations/:idConversation detail + full transcript.
POST/api/v1/conversations/:id/messagesSend a message as system/role. notify:false to mirror without a webhook.
POST/api/v1/conversations/:id/closeClose the conversation.
GET/c/:tokenParticipant chat UI (HTML).
GET/c/:token/infoConversation info + the other party's label.
GET/c/:token/messagesList messages.
POST/c/:token/messagesParticipant sends a message.
GET/c/:token/streamRealtime SSE.
GET/widget.jsSupport-widget loader. Public, no key — takes data-site=<widget_key>.
POST/w/:widgetKey/sessionPublic. { visitor_id }{ token }: opens or restores that visitor's conversation.

1. Create a conversation

buyer.name/seller.name is the label the other party sees — anonymize here.

POST https://znatoka.com/api/v1/conversations
X-Api-Key: <tenant api_key>
Content-Type: application/json

{
  "external_ref": "REFUND-PR05001",
  "subject": "Refund: Gmail can't log in",
  "buyer":  { "name": "Buyer #4821" },
  "seller": { "name": "Seller (anonymous)" }
}

201
{ "id": "...", "buyer_url": "https://znatoka.com/c/...", "seller_url": "https://znatoka.com/c/..." }

2. Webhook on incoming message optional

The chat works without a webhook — any party with the chat page or embed widget open receives messages in realtime over SSE. A webhook only adds server-to-server push, so you can alert a party who is not currently viewing the chat. Skipping it? Rely on the embed widget, or poll GET /api/v1/conversations/:id.

To enable it, set the tenant webhook_url in the Portal. On each incoming message we POST the event below; use open_conversation_url to notify notify_role.

POST <your tenant webhook_url>
X-Chatdisiniaja-Signature: sha256=<HMAC-SHA256(body, webhook_secret)>

{
  "event": "message.created",
  "conversation_id": "...", "external_ref": "REFUND-PR05001",
  "sender_role": "buyer",
  "preview": "Hi, the account won't log in...",
  "notify_role": "seller",
  "open_conversation_url": "https://znatoka.com/c/<token>"
}

3. Verify the webhook signature (Node.js)

Only needed if you use webhooks. Compute the HMAC over the raw request body, before JSON parsing.

const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const sig = (header || '').replace(/^sha256=/, '');
  const exp = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return sig.length === exp.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(exp));
}