For developers

Connect Nata to your own site

You own the website code or the customer-service system? Pick one of two paths: paste the widget into your store page, or call the server-to-server API from a system that already has its own chat UI. Both run the same Nata as the chat on app.nyataka.com.

Get a key in the app Request the API package
Separate package

The API is not part of a subscription — here is what it costs

The API is its own package, separate from the web/Telegram subscription (Pro or Max). Buying Max does not include the API: a Max subscription is not a prerequisite and not the purchase. The storefront widget is a Max plan feature; the API is not part of any plan.

Rp 199per inbound message, prepaid. No monthly fee, no monthly minimum.


These prices are an owner decision from 2026-10-03 (docs product-strategy-tiers-api §4). Billing is still manual: no self-serve top-up in the app and no automatic invoice. Limits are in section 7.

1. Two paths, pick one

Widget on your site

One <script> line shows Nata as a panel on the right side of your site. Shoppers ask questions and get answers from your catalog and policies. You write no backend code.

The key is the store's public key (nata_pub_…). It is safe in HTML: it can only answer from your catalog, and only from the domains you allow.

API server-to-server

Your system or helpdesk already has a chat box? Send the message to POST /webhook/nata and render Nata's reply in your own UI. It fits helpdesks, WhatsApp gateways, and internal ERP tools.

The secret key (nata_whk_…) belongs on your server only. Never ship it in browser code.

You can use both: the widget for shoppers on your store pages, the API for your support team's internal dashboard.

2. API keys

Open api.nyataka.com → Kunci and click Buat kunci. The key is shown once. Our server keeps only its hash, so save it somewhere safe right then.

The format:

nata_whk_<key_id>.<rahasia>

key_id is the public identifier, safe to quote in a support ticket. The secret half must never leave your server. Send both on every request:

Authorization: Bearer nata_whk_<key_id>.<rahasia>

Don't confuse it with the widget key

The official widget uses a public key, nata_pub_…, which may sit in HTML and can only call the widget. The API uses nata_whk_…, which is secret, server-side only, and spends API credits. A widget key cannot call /webhook/nata, and an API key must never be used in a widget script.

Rotate a key

Click Ganti kunci on the same page. The old key dies immediately, with no transition period, so update the environment variable on your server as soon as you rotate. Need to rotate with no downtime? Ask us: two active keys at once are not supported today.

If a key leaks

  1. Rotate the key now on the API page. The old key dies at once.
  2. Update NATA_API_KEY on your server and redeploy.
  3. Check usage on the API page (messages today) for traffic that is not yours.
  4. Email [email protected] with the key_id, not the key, so we can trace the logs.

A key pasted into frontend code is the most common cause. If the browser can read the key, treat it as leaked.

3. Quickstart

Every integration endpoint, at a glance:

EndpointAuthWhat it is for
POST /webhook/nataBearer nata_whk_…Send one message, get a reply. Synchronous, or 202 plus a callback.
GET /webhook/nata/usageBearer nata_whk_…Your own meter: today's messages, tokens, remaining credits.
PUT /webhook/nata/knowledgeBearer nata_whk_…Push product and policy pages from your backend.
GET /api/embed/configpublic key + a registered originWidget config. The widget calls it; handy for a quick check.
POST /api/embed/shop-chatpublic key + a registered originA shopper question to the widget.

Rotate keys, set the callback URL, and read the meter on the API surface itself: api.nyataka.com → Kunci. There is no endpoint to rotate a key from your server. That is deliberate: leaked code must not be able to replace the key.

curl

curl -X POST https://app.nyataka.com/webhook/nata \
  -H "Authorization: Bearer $NATA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Stok gamis ukuran L masih ada?","thread_id":"cust-123"}'

JavaScript (Node 18+, built-in fetch)

const res = await fetch("https://app.nyataka.com/webhook/nata", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.NATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ text: "Stok gamis L masih ada?", thread_id: "cust-123" }),
});

const data = await res.json();
if (!res.ok) {
  // data.error holds the machine code; see section 8
  throw new Error(data.error);
}
console.log(data.reply); // the reply is ready to display

Request body

FieldRequiredMeaning
textyes, unless you send an attachmentThe inbound message. Truncated at 8,000 characters. message works as an alias.
thread_idnoYour conversation label, such as a ticket or customer id. Echoed back unchanged.
modeno"sync" (default) or "async". See section 4.
attachmentsno[{ filename, mime, content_base64 }]. At most 4 files, 4 MB each.

Response body (200)

{
  "ok": true,
  "reply": "Masih ada 4 pcs ukuran L.",
  "reply_html": "<p>Masih ada 4 pcs ukuran L.</p>",
  "reply_text": "Masih ada 4 pcs ukuran L.",
  "backend": "internal",
  "thread_id": "cust-123",
  "usage": { "messages_today": 12, "input_tokens": 812, "output_tokens": 233, "messages_this_month": 640 },
  "credits": 488
}

reply is Nata's Markdown. reply_html is rendered HTML, and reply_text is the same text without Markdown. Pick whichever fits your UI. usage.input_tokens/output_tokens are for your own cost reconciliation, not a unit we bill. messages_this_month tracks the volume threshold. backend is an internal label: do not build logic on it.

Your own meter

curl https://app.nyataka.com/webhook/nata/usage \
  -H "Authorization: Bearer $NATA_API_KEY"
{
  "ok": true,
  "day": "2026-10-04",
  "resets_wib": "07:00",
  "credits": 488,
  "usage": { "messages": 12, "input_tokens": 8120, "output_tokens": 2330 }
}

4. Async mode and callbacks

A heavy request, such as an analysis or a long report, can outlast a waiting user. Send mode: "async": we answer 202 right away and POST the result to the callback URL saved on the API page.

await fetch("https://app.nyataka.com/webhook/nata", {
  method: "POST",
  headers: { Authorization: "Bearer " + process.env.NATA_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ text: "Buat laporan penjualan minggu ini", thread_id: "ops-9", mode: "async" }),
});

The callback body is the same JSON as a synchronous reply (reply, reply_html, reply_text, thread_id), sent as a POST to your URL.

Verify the callback signature

Every callback is signed, so knowing your callback URL is not enough to inject a fake reply. The secret is callback_signing_secret on the API page. It is issued with the key and does not change when you rotate it; GET /api/web/webhook/status shows it again if you lose it. We send two headers:

HeaderValue
X-Nyataka-TimestampUnix seconds when we signed. Reject the request if the difference is more than 5 minutes.
X-Nyataka-Signaturesha256=<hex>, an HMAC-SHA256 of <timestamp>.<raw body> with that secret.

Verification in Node. Use the raw body, before JSON.parse:

import crypto from "node:crypto";

// Signature = HMAC-SHA256(secret, `${X-Nyataka-Timestamp}.${rawBody}`)
export function verifyCallback(rawBody, headers, secret) {
  const timestamp = Number(headers["x-nyataka-timestamp"]);
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = "sha256=" + crypto.createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(headers["x-nyataka-signature"] || "");
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

5. Push your own catalog

Nata answers shoppers from your store data. Two ways to fill it: let Nata read your site sitemap from the Akun tab (Baca katalog dari website), or push the data from your backend. Pushing is more accurate for stock and prices that change fast.

curl -X PUT https://app.nyataka.com/webhook/nata/knowledge \
  -H "Authorization: Bearer $NATA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "replace",
    "products": [
      { "id": "gm-1042", "title": "Gamis Lyla", "price_idr": 189000, "in_stock": true,
        "category": "gamis", "url": "https://tokomu.com/p/gamis-lyla",
        "description": "Bahan viscose, ukuran L dan XL." }
    ],
    "pages": [
      { "id": "retur", "title": "Kebijakan pengembalian", "url": "https://tokomu.com/retur",
        "body": "Retur 7 hari, barang belum dipakai." }
    ]
  }'
Item fieldRequiredMeaning
titleyesThe product or page name. Items without title are skipped.
idnoYour stable id. Used by mode: "upsert". Without it, url or title is the key.
price_idrnoPrice in rupiah, as a number with no separators.
in_stocknotrue or false. Nata uses it to say "out of stock" honestly.
urlnoLink to the product page.
description / bodynoText Nata reads when it answers.
categorynoAny category you like, such as gamis.

What you get: Nata answers from your real prices, stock, and policies instead of guessing. If you never pushed a catalog, Nata says the data is missing. That is correct behaviour, not a bug.

6. The widget and allowed origins

Get the snippet at app.nyataka.com → Integrasi → Widget di situs after you fill in the store name and domain list. It looks like this:

<script src="https://app.nyataka.com/embed/nata-widget.js"
        data-key="nata_pub_KUNCI_PUBLIK_TOKO"
        data-mode="shopper" defer></script>
AttributeDefaultMeaning
data-key—The store public key (nata_pub_…). Required in shopper mode.
data-modeshoppershopper for buyers. seller for you on an admin page: sign in with your Nyataka account and the widget can read that page's text.
data-titleAsk NataPanel title.
data-color#1d1c1dAccent colour (#hex).
data-pushfalse"true": push the page content aside instead of covering it.
data-openfalse"true": the panel opens on page load.

From your page's JavaScript: window.NataWidget.open(), .close(), .toggle(), and .ask("text").

Endpoints the widget calls

Use these to debug the network tab or build your own shopper UI:

EndpointValue
GET /api/embed/config?key=nata_pub_…Store name, contact, greeting, and suggested questions. Useful for a quick check that the key and origin are right.
POST /api/embed/shop-chatBody { key, message, history? }. Response { ok, reply, reply_html, products }.

Both take the public key and serve only the origins you allow. They are not a replacement for the server-to-server API, and they reject nata_whk_ keys.

CORS and the origin list

A shopper request is served only when the browser's Origin header matches a row in Website yang boleh memasang widget exactly. Write the scheme and host only, with no path and no trailing slash:

https://tokumu.com
https://www.tokumu.com

An unlisted origin gets 403 origin_not_allowed and no CORS header. In a browser that usually shows up as a CORS error in the console, not as a widget error message. It is the most common red herring, so check the origin list first.

A different subdomain is a different origin. https://www.tokumu.com does not inherit the allowance from https://tokumu.com. Add both if shoppers arrive from both.

Widget limits

6b. Page copilot (page agent)

If the admin page you want help with is your own, send mode: "copilot" to the same endpoint. You send what is visible on the page; Nata replies with words plus a list of actions the page may run. Nata has no browser here — your page executes them.

POST /webhook/nata
Authorization: Bearer <API_KEY>

{
  "mode": "copilot",
  "session_id": "percakapan-1",
  "message": "Ubah nama toko jadi Rumaisha Hijab",
  "access_mode": "approval",
  "page": {
    "url": "https://admin.tokomu.com/pengaturan",
    "title": "Pengaturan Toko",
    "summary": "Form pengaturan. Nama toko: Lama.",
    "fields": [{ "name": "store_name", "label": "Nama toko", "value": "Lama" }],
    "elements": [{ "ref": "r1", "role": "button", "name": "Simpan" }]
  }
}

The response carries reply, actions, requires_approval, and session_id. The only allowed actions are set_field (the name must exist in page.fields) and click (the ref must exist in page.elements). The server drops Save/Publish/Delete/Pay/Send actions; the page must still show a diff and ask for approval when requires_approval is true.

access_mode: read_only (no actions), approval (default; actions wait for approval), full_access (actions may run immediately). One copilot call costs one API credit. The full guide and a page-agent example live in docs/copilot-api.md; this contract is for every API customer, not one store.

If your page has many routes, declare the manifest once. Real Rumaisha example: 20 routes, 86 fields, 11 flows; the pack is at docs/rumaisha-page-agent-pack.json. A manifest stores structure, never field values, passwords, or buyer data.

7. API limits and quota

LimitsValueWhat happens
text length8,000 charactersTruncated, not rejected.
JSON body size8 MBThe request is rejected at the server before it reaches Nata.
Attachments4 files, 4 MB each400 too_many_attachments / attachment_too_large.
Attachment typesJPEG, PNG, WebP, GIF, PDF, text, CSV, JSON, XLSX, WAV, MP3, OGG, FLAC, AAC400 unsupported_attachment_type.
API daily message ceiling3,334 messages/day, derived from the 100,000/month volume. An operator can raise it per account.429 daily_chat_quota. Resets at 07:00 WIB. This is not your plan's chat quota.
API creditsprepaid, 1 message = 1 credit402 quota_exhausted; the response includes the remaining credits.
Per-minute limit per keynone yet—

Two fences, two counters. API messages use the API's own daily quota, not your plan's daily chat quota: web chat and the API do not consume each other's allowance. The two fences are the API daily ceiling above (429 daily_chat_quota) and the prepaid balance of one credit per message (402 quota_exhausted). A key with no prepaid balance — an old key created before credits existed — does not spend a credit, but the API daily ceiling still applies. Your account's real numbers are on the API page.

Pricing

In short, from Pricing and packages above: the API is a separate package, prepaid at Rp 199 per message, with no monthly fee.

UsagePriceNote
Inbound text messageRp 1991 message = 1 reply. Minimum top-up Rp 250,000.
Volume, from 50,000 messages/monthRp 175Separate agreement for partners.
Volume, from 100,000 messages/monthRp 149Separate agreement for partners.

These prices are an owner decision from 2026-10-03 (docs product-strategy-tiers-api §4). Billing is still manual through the team: no self-serve top-up in the app and no automatic invoice. To add credits, email [email protected] with your key_id.

8. Errors

Every error is JSON: { "error": "machine_code" }. error is the stable contract and never changes language. When there is human text it arrives twice: message in Indonesian and message_en in English. Some responses also carry credits or required. These are the codes the API emits today.

API webhook

HTTPCodeWhat it means and what to do
401missing_api_keyThe Authorization header is missing or not in Bearer format.
401invalid_api_keyThe key is wrong, rotated, or stale. Create a new key on the API page.
400invalid_textThe body has no text and no attachment.
402quota_exhaustedThe prepaid balance is empty. The response includes credits. Ask the team for a top-up.
400callback_url_required_for_asyncmode: "async" was used with no callback URL saved.
429daily_chat_quotaThe API daily message ceiling is reached. The code name says "chat"; on the API path it means the API ceiling. Resets at 07:00 WIB.
403webhook_channel_scale_onlyThis account has no API subscription yet. The code name mentions a plan for historical reasons; read required: "api_subscription" and message/message_en in the body instead.
403account_disabledThe account is disabled. Contact support.
401unknown_sellerThe key points at an account that no longer exists.
502agent_failedNata failed to answer. The credit is refunded; try again.
504agent_timeoutNata ran out of time. The credit is refunded; try again or use mode: "async".
400page_cannot_commitThe page has no save. Use read-only, or add a commit handler in the page agent.
400attachments_not_supportedCopilot mode does not accept attachments; send only page and message.
501copilot_not_configuredThis deployment has not enabled copilot. Contact the team.
400too_many_attachments, attachment_too_large, unsupported_attachment_type, empty_attachment, bad_attachmentThe attachment was rejected. Check the count, size, and type in section 7.

Keys and callbacks (from the API page)

HTTPCodeMeaning
403webhook_channel_scale_onlyThis account has no API subscription yet.
404webhook_key_missingSaving a callback URL before a key exists.
400callback_url_must_be_httpsThe callback URL is not https.
400invalid_callback_urlThe URL is not public (localhost, a private IP, .local) or cannot be parsed.

Catalog

HTTPCodeMeaning
401invalid_api_keyThe key is not recognised. A store public key counts as unrecognised here: use a nata_whk_ key for the catalog.
400nothing_to_storeNo item had a title that could be stored.

Widget

HTTPCodeMeaning
400invalid_keydata-key is missing or not in nata_pub_… format.
404widget_unavailableThe key is not registered, or the widget is switched off on the widget page.
403origin_not_allowedThe sending origin is not in the allowed list.
403widget_plan_requiredThe account's plan does not include the widget.
400invalid_messageThe message is empty.
413message_too_longThe message is over 1,600 characters.
429slow_downThe per-IP or per-site limit is reached. Try again shortly.
429store_daily_capThe store's daily shopper quota is used up.
502unavailableNata failed to answer. The shopper quota is refunded.

9. Troubleshooting

SymptomMost likely causeFix
401 invalid_api_key every timeThe key was copied incompletely, quotes or spaces came with it, or the key was rotatedCopy it again from the API page. The key is shown once, so create a new one if needed, and make the header exactly Bearer nata_whk_…
The widget does not appear and the console is full of CORS errorsThe origin is not listed, or it was written with a path or trailing slashAdd https://host alone to the origin list and reload the page
404 widget_unavailableThe public key belongs to another store, or the widget is offCheck data-key and tick "Aktifkan widget untuk pembeli"
403 widget_plan_requiredThe plan is downgraded or pendingCheck the plan on the Akun page; contact the team if you just paid
The callback never arrivesThe URL is not public https, or your callback server is slow or redirectsUse public https, answer 2xx fast, and remember we try only 2 times
The callback arrives twiceYou set a callback URL and use synchronous modePick one: clear the callback URL, or use mode: "async"
429 daily_chat_quota even though the API is newThe API daily ceiling is reached, derived from the 100,000/month volume. It is not your plan's chat quotaWait for the 07:00 WIB reset, or ask for a higher daily ceiling at [email protected]
402 quota_exhaustedThe prepaid balance is emptyAsk for a top-up at [email protected]
Nata says the data is missingNo catalog was ever pushed or importedPUT /webhook/nata/knowledge, or "Baca katalog dari website" on the widget page
The reply is cut offThe 8,000-character limit on the inbound messageSend a shorter message, or summarise on your side

10. Support

Email [email protected]. Include the key_id, not the secret, plus the time it happened (WIB), the endpoint you called, and the raw response body. For a credit top-up, put "Top up kredit API" in the subject.