Developer docs
External API
Authenticate with a project API key and push knowledge or products into your owni.chat AI assistant from your CMS, ERP, or backend.
Overview
The external API is a project-scoped REST interface. Every request must include a valid API key in the Authorization header. Keys are created in the dashboard under API, belong to exactly one project, and can be revoked at any time.
- Base path:
https://app.owni.chat/api/external/v1 - Auth:
Authorization: Bearer ak_live_… - Rate limit: 60 requests per minute per client
- Content type for JSON endpoints:
application/json - Outbound events: signed webhooks from Integrations → Webhook (see Webhooks)
Authentication
All external endpoints require a project API key. This is not your dashboard login password or JWT session token — it is a dedicated server-to-server secret issued per project.
1. Create a key in the dashboard
- Sign in to your owni.chat account and open the target workspace / project.
- Open API in the project sidebar.
- Enter a label (for example “CMS sync” or “Staging ERP”) and create the key.
- Copy the full key immediately. For security it is shown only once. After you leave the page, the dashboard only shows a short prefix such as
ak_live_a1b2c3….
2. Key format
Newly issued keys look like:
ak_live_<48 hex characters>- Prefix
ak_live_identifies a live project API key. - The secret part is 24 random bytes encoded as hex (48 characters).
- Keys that do not start with
ak_are rejected with401.
3. Send the Bearer token
Pass the full raw key on every request:
Authorization: Bearer ak_live_your_api_keycurl -X GET \
https://app.owni.chat/api/external/v1/projects/YOUR_PROJECT_ID/products \
-H "Authorization: Bearer ak_live_your_api_key"- Scheme must be
Bearer(capital B) followed by a single space and the key. - Do not send the key as a query parameter, cookie, or custom header other than
Authorization. - Missing or malformed headers return
401 Missing API key. Use Authorization: Bearer <key>.
4. How the server validates the key
owni.chat never stores the raw key. On creation we save only:
- SHA-256 hash of the full key — used for lookup on each request
- Key prefix — short public identifier for the UI
- Scopes, optional expiry, active/revoked flag
On each call the middleware hashes the Bearer value and loads the matching active key. Then it checks expiry, required scope, and that the key’s project_id matches :projectId in the URL. Successful use updates last_used_at.
5. Project binding
Every key is permanently tied to one project UUID. Paths always include that project:
https://app.owni.chat/api/external/v1/projects/{projectId}/…If you call another project’s ID with this key, the API returns 403 API key does not belong to this project. Create a separate key inside each project you need to automate.
6. Expiry and revocation
- Keys may be created with an optional
expires_attimestamp. After that moment requests fail with401 API key has expired. - Revoking a key in the dashboard sets it inactive immediately. Further requests return
401 Invalid or revoked API key. Revocation cannot be undone — create a new key if needed. - Because only the hash is stored, a lost raw key cannot be recovered. Rotate by creating a new key, updating your backend, then revoking the old one.
Typical auth error responses
| Status | Example message | Cause |
|---|---|---|
| 401 | Missing API key. Use Authorization: Bearer <key> | No or non-Bearer Authorization header |
| 401 | Invalid API key | Value does not start with ak_ |
| 401 | Invalid or revoked API key | Unknown hash or revoked key |
| 401 | API key has expired | expires_at is in the past |
| 403 | API key is missing required scope: … | Key lacks the scope for this endpoint |
Scopes
Each key carries a list of scopes. New keys currently receive both scopes by default. The endpoint still checks the scope required for that route.
| Scope | Allows |
|---|---|
| knowledge.write | Create knowledge sources, upload training files, trigger re-index |
| products.write | List, get, upsert, bulk upsert and delete catalog products |
Base URL & project ID
Production API root:
https://app.owni.chat/api/external/v1Replace YOUR_PROJECT_ID with the project UUID from the dashboard (project settings / URL). The same value must match the project that owns the API key.
Add knowledge (text or URL)
Create a knowledge source. Embeddings are rebuilt in the background — usually within about a minute. Requires knowledge.write.
Plain text or FAQ
curl -X POST \
https://app.owni.chat/api/external/v1/projects/YOUR_PROJECT_ID/knowledge-sources \
-H "Authorization: Bearer ak_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"type": "text",
"name": "Shipping policy",
"raw_text": "We ship worldwide within 2-5 business days..."
}'Web page by URL
curl -X POST \
https://app.owni.chat/api/external/v1/projects/YOUR_PROJECT_ID/knowledge-sources \
-H "Authorization: Bearer ak_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"type": "url",
"name": "Help center article",
"source_url": "https://example.com/help/returns"
}'Supported type values include text, faq, url, website, and product_feed (XML feed URL with optional metadata).
Response 201:
{
"knowledge_source": {
"id": "6a1f...c2",
"type": "text",
"name": "Shipping policy",
"status": "pending",
"created_at": "2026-07-09T12:00:00.000Z"
}
}Upload files
PDF, TXT, Markdown and CSV files are parsed and indexed automatically. Send multipart/form-data. Max size 10 MB. Requires knowledge.write.
curl -X POST \
https://app.owni.chat/api/external/v1/projects/YOUR_PROJECT_ID/knowledge-sources/upload \
-H "Authorization: Bearer ak_live_your_api_key" \
-F "[email protected]" \
-F "name=Product manual"Re-index a source
Re-fetch or rebuild embeddings for an existing source without recreating it. Requires knowledge.write.
curl -X POST \
https://app.owni.chat/api/external/v1/projects/YOUR_PROJECT_ID/knowledge-sources/SOURCE_ID/reindex \
-H "Authorization: Bearer ak_live_your_api_key"Products catalog
Manage shop products used for AI product cards and add-to-cart. All product routes require products.write. Identify items by your shop external_id (SKU / product_id from the feed).
| Method | Path | Purpose |
|---|---|---|
| GET | /projects/:id/products | List products (page, page_size) |
| GET | /products/:externalId | Get one product |
| PUT | /products/:externalId | Create or update one product |
| POST | /products/bulk | Upsert up to 100 products |
| DELETE | /products/:externalId | Delete a product |
Upsert example
curl -X PUT \
https://app.owni.chat/api/external/v1/projects/YOUR_PROJECT_ID/products/41494 \
-H "Authorization: Bearer ak_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Demo product",
"url": "https://shop.example/?product_id=41494",
"price": "199.00",
"currency": "UAH",
"image_url": "https://shop.example/img/41494.jpg"
}'Live catalog search
For large shops, do not dump every SKU into owni.chat by hand. Host this search API. The assistant queries it in chat, and owni.chat also pages the same endpoint (empty q + offset) to fill the project catalog.
Endpoint your site exposes
GET https://your-shop.example/search?q=smok&limit=8&max_price=1200&locale=uk
Authorization: Bearer YOUR_TOKENSuccess body
{
"owni": true,
"locale": "uk",
"products": [
{
"external_id": "41494",
"title": "Smok Nord 5",
"price": "1190",
"currency": "UAH",
"url": "https://your-shop.example/?product_id=41494",
"image_url": "https://your-shop.example/img.jpg",
"availability": "in_stock",
"brand": "Smok",
"category": "POD",
"attributes": [
{"name": "Battery", "value": "2000 mAh"},
{"name": "Model", "value": "Nord 5"}
]
}
]
}Ping (empty q) must return { "owni": true }and may already include the first catalog page. external_id is the id your add-to-cart JS expects.
Pull the full catalog
After you connect the URL, owni.chat pages the same endpoint with empty q and increasing offset (50 per request, up to 10 000 SKUs) and writes products into Knowledge → catalog. Repeat every 6 hours, or click Sync now on the integration.
GET https://your-shop.example/search?q=&limit=50&offset=0&locale=uk
Authorization: Bearer YOUR_TOKEN
{
"owni": true,
"locale": "uk",
"total": 4821,
"products": [ { "external_id": "41494", "title": "…", "url": "https://…" } ]
}Implement listing when q is empty: stable order, limit up to 100, offset skip. Optional total lets paging stop early. Search (non-empty q) stays a relevance query with limit 1–50.
Product fields
Required: external_id, title, url. Send specs in attributes so the assistant can answer battery, flavour, size, compatibility — not only name and price. Up to 50 pairs; names stay in the catalog language (do not translate SKUs).
| Field | Required | Notes |
|---|---|---|
| external_id | yes | Shop product id for add-to-cart |
| title, url | yes | Product page URL must be http(s) |
| price, currency, image_url | no | Shown on the product card |
| availability, brand, category | no | in_stock / out_of_stock |
| attributes | no | [{ "name": "Battery", "value": "2000 mAh" }] — specs the model cites in the reply |
Connect in the dashboard
- Open the project → Integrations → Live catalog search.
- Paste the search URL and bearer token from your plugin or custom API.
- Pick the catalog language (or keep auto from ping
locale). - Save — owni.chat probes the URL, shows sample products, and turns the tile green only when the JSON is valid.
Official plugins already expose this API. OpenCart: /index.php?route=extension/module/owni_chat_api&action=search. WooCommerce: /wp-json/owni/v1/search. Shopify stores that are already connected under Integrations → Shopify are searched via the Admin API without a second URL.
Best practices so search actually finds the product
The assistant never dumps the whole catalog into the model. On each shopping question it rewrites the visitor text into a few short shop queries, calls your search URL (up to ~50 hits per query), then ranks in-stock first and prefers SKUs that match more of the visitor's words (flavour, vitamins, 0 mg — not only the category “disposable”). If your endpoint only matches the product title, questions like “одноразки з вітамінами” will miss Nutriair and return popular Elf Bars instead.
What the shop search should do
- Match
qagainst title, SKU/model, URL slug, and attributes (flavour, nicotine, battery, “вітаміни / B Complex”). Storefront search that ignores attributes is the #1 reason the bot looks “blind”. - Treat hyphens and spaces as the same token (
малина лимон=малина-лимон). Stem inflections:банана→банан,витаминами→витамин. - Always send honest
availability:in_stockorout_of_stock. The assistant will still mention OOS SKUs if the visitor named that exact product, but it leads with what you can sell today. - Return enough rows. Honour
limitup to 50. If you only send the first 8 “best sellers”, niche in-stock matches never reach the chat. - Put the distinctive words in the title or attributes, not only in a long HTML description. “Одноразовая витаминная электронная сигарета Nutriair” will rank; “Elf Bar 30000” with vitamins buried in a blog block will not.
- Empty
qmust list the catalog (stable order +offset). That fill is what powers compare, similar in-stock, and follow-up questions when live search is thin. - Same
external_idas add-to-cart. Wrong ids = cards that cannot be purchased.
Queries owni.chat actually sends
For one visitor message we fire several GETs in parallel (rewritten pair + the original phrase + extras for special intents), then merge. Examples:
| Visitor says | Typical shop q values |
|---|---|
| Є одноразки з вітамінами? | вітамін однораз, Nutriair вітамін, plus the original sentence. Your API should hit titles/attributes containing “вітамін”, not only “одноразка”. |
| Є 420 малина лимон 50 мг? | 420 малина-лимон 50, 420 малина лимон. Keep hyphenated flavours in one token; do not require the whole title. |
| Без нікотину / 0 мг / безнекатина | Fixed extras: 0 мг and без нікотину. Match attribute values 0 мг / 0mg, not kits that only say “850 mAh”. |
| Smok Nord 5 до 1200 | smok nord 5, nord 5, with max_price=1200 when you implement the price params. |
| Подик Elf Bar (device, not a liquid) | Brand + line tokens. Filter out cartridges/coils in your search if the query is clearly a kit/device. |
# One chat turn — several GETs, then merge (in-stock first)
GET /search?q=вітамін%20однораз&limit=50&locale=uk
GET /search?q=Nutriair%20вітамін&limit=50&locale=uk
GET /search?q=а%20одноразки%20с%20витаминами%20есть&limit=50&locale=ukPing with empty q is not a search: it is catalog paging. Do not return a random “featured” 8 SKUs there.
Webhooks
Webhooks are the opposite direction of the REST API: owni.chat POSTs chat events to a URL you configure. Use them for Zapier, Make, n8n, HubSpot/Pipedrive via middleware, or your own backend. They are not authenticated with ak_live_… — instead each delivery is signed with a per-project signing secret.
1. Connect an endpoint
- In the dashboard open the project → Integrations → Webhook.
- Paste an HTTPS URL that accepts
POSTwith a JSON body (Zapier Catch Hook, Make webhook, or your own route). - Save. Copy the signing secret (
whsec_…) immediately — it is shown only on connect or after you rotate it. - Click Send test to deliver a
testevent and confirm your endpoint returns 2xx.
Store the secret as an environment variable (for example OWNI_WEBHOOK_SECRET). Never put it in client-side code.
2. Payload & headers
Every delivery is a single POST with:
Content-Type: application/jsonX-Owni-Timestamp— Unix seconds when the request was signedX-Owni-Signature—sha256=<hex>HMAC over<timestamp>.<raw body>
Body shape
{
"event": "lead_captured",
"data": { … },
"sent_at": "2026-07-27T18:08:48.425Z"
}3. Verify the signature
Always verify before trusting the payload:
- Read the raw request body as a string. Do not
JSON.parseand re-stringify before hashing — byte order must match what was signed. - Compute
HMAC-SHA256(secret, timestamp + "." + rawBody)and prefix withsha256=. - Compare to
X-Owni-Signaturein constant time. - Reject if
|now − timestamp| > 300seconds (replay protection).
Helper packages: @ownichat/sdk/webhooks (parseWebhookRequest) and @ownichat/next/server (createWebhookHandler).
4. Events
| Event | When it fires | data fields |
|---|---|---|
| test | Dashboard → Send test | message |
| lead_captured | Lead-capture form in a chat flow | flow_name, node_label, conversation_id, project_id, fields |
| conversation.started | New visitor conversation | conversation_id, project_id, visitor_id, mode, source_page_url |
| conversation.closed | Visitor or operator closes the chat | conversation_id, project_id, visitor_id, closed_by |
| handoff.requested | Escalation to a human operator | conversation_id, project_id, visitor_id, reason, mode |
handoff.requested reason is one of visitor, anger, flow, ai_failure. Flow builder Webhook nodes may also send a custom event name (default flow_webhook) with flow_id, node_id, and optional contact fields.
Example: lead_captured
{
"event": "lead_captured",
"data": {
"flow_name": "Homepage lead",
"node_label": "Contact form",
"conversation_id": "3f2a…",
"project_id": "9c1b…",
"fields": [
{ "id": "email", "label": "Email", "type": "email", "value": "[email protected]" },
{ "id": "name", "label": "Name", "type": "text", "value": "Ann" }
]
},
"sent_at": "2026-07-27T18:08:48.425Z"
}5. Receive examples
Next.js App Router
// app/api/owni/webhook/route.ts
import { createWebhookHandler } from '@ownichat/next/server';
export const POST = createWebhookHandler({
onLead: async (fields) => {
await db.lead.create({ data: fields });
},
onConversationStarted: async (data) => {
await analytics.track('chat_started', data);
},
onHandoff: async (data) => {
await notifyOps(data);
},
onConversationClosed: async (data) => {
await analytics.track('chat_closed', data);
},
});
// Set OWNI_WEBHOOK_SECRET=whsec_… in the server environment.Node / any runtime with the SDK
import { parseWebhookRequest, WEBHOOK_EVENTS } from '@ownichat/sdk/webhooks';
export async function POST(request) {
const body = await request.text(); // raw body — not request.json()
const result = parseWebhookRequest({
body,
headers: request.headers,
secret: process.env.OWNI_WEBHOOK_SECRET,
});
if (!result.valid) {
return new Response(result.reason, { status: 401 });
}
switch (result.event.event) {
case WEBHOOK_EVENTS.LEAD_CAPTURED:
// result.event.data.fields
break;
case WEBHOOK_EVENTS.CONVERSATION_STARTED:
case WEBHOOK_EVENTS.CONVERSATION_CLOSED:
case WEBHOOK_EVENTS.HANDOFF_REQUESTED:
break;
}
return new Response('ok');
}6. Delivery rules
- One attempt per event, 10 second timeout.
- Respond 2xx quickly; do heavy work asynchronously so the queue does not mark the delivery as failed.
- Last delivery time and last error are shown under Integrations → Webhook.
- Rotating the secret invalidates the old one immediately — update your receiver first, then rotate.
- Native HubSpot / Pipedrive lead sync is separate (Integrations tiles). Webhooks remain the generic path for any other CRM or automation tool.
Errors & limits
| Status | Meaning |
|---|---|
| 401 | Missing, invalid, revoked or expired API key |
| 403 | Key does not belong to this project or lacks the required scope |
| 400 | Validation error — check the response message |
| 404 | Resource not found (source, product, …) |
| 429 | Rate limit exceeded (60 requests per minute) |
Error bodies are JSON objects with a message string. Validation failures may also include an errors array with field details.
Security best practices
- Call the API only from your backend or trusted automation. Never embed
ak_live_…in browser JavaScript, mobile apps, or public repos. - Store keys in a secret manager or environment variables, not in source control.
- Use separate keys per environment (production vs staging) and revoke unused keys.
- If a key leaks, revoke it in the dashboard immediately and issue a replacement.
- Prefer short-lived keys (
expires_at) for temporary integrations and contractors. - Monitor
last_used_atin the dashboard to spot unexpected activity.
Ready to automate training?
Create a free account, open your project, and generate an API key in minutes.
Start free