Skip to main content
aichat.mn

Developers

REST API and webhooks

Connect orders, products, appointments, contacts and conversations to your own store, ERP or CRM, and receive every event through signed webhooks.

  • JSON · HTTPS
  • 120 requests per minute per key
  • In the Pro plans

Your first request

curl
curl https://aichat.mn/api/v1/orders?page=1&pageSize=20 \
  -H "Authorization: Bearer ak_live_…"

1Overview

The API accepts and returns JSON. Your organization is identified by the API key, so URLs never contain your organization’s name.

Base URL

URL
https://aichat.mn/api/v1
  • Send Content-Type: application/json with every request that has a body.
  • Dates are ISO 8601 in UTC: 2026-11-04T09:30:12.000Z.
  • Amounts are whole tögrög: 94000 means 94,000₮.
  • Lists are paginated: ?page=1&pageSize=20 → a data array plus total, page and pageSize. Messages use a before cursor and limit.

2Authentication and scopes

Send your organization’s API key in the Authorization header with every request.

  1. 1

    Create a key

    In your dashboard, open “Settings → API & webhooks”, click “Create key”, name it and pick the scopes it needs.

  2. 2

    Copy it once

    The ak_live_… key is shown only once, when it’s created — we store only its hash.

  3. 3

    Add the header

    Send Authorization: Bearer ak_live_… with every request.

Scopes

A key can only call endpoints covered by its scopes. A missing scope returns 403 with the SCOPE_MISSING code.

ScopeAllowsEndpoints
contacts:readRead contacts and their details
  • GET/contacts
  • GET/contacts/:id
contacts:writeUpdate contact details
  • PATCH/contacts/:id
conversations:readRead conversations and messages
  • GET/conversations
  • GET/conversations/:id
  • GET/conversations/:id/messages
messages:sendSend messages into conversations
  • POST/conversations/:id/messages
orders:readRead orders
  • GET/orders
  • GET/orders/:id
orders:writeCreate orders and change their status
  • POST/orders
  • PATCH/orders/:id
products:readRead the catalog
  • GET/products
  • GET/products/:id
products:writeAdd and update products
  • POST/products
  • PATCH/products/:id
bookings:readRead appointments and free slots
  • GET/bookings
  • GET/bookings/:id
  • GET/booking-services
  • GET/booking-services/:id/slots
bookings:writeCreate appointments
  • POST/bookings

3Rate limits and errors

Each key can make up to 120 requests per minute. The X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers on every response show the limit, the requests left and when the limit resets. Beyond that you get 429, and the Retry-After header tells you how many seconds to wait.

Error format

Every error returns JSON with the HTTP status, a machine-readable code and a message in Mongolian:

JSON
{
  "statusCode": 403,
  "code": "SCOPE_MISSING",
  "message": "Энэ түлхүүрт orders:write эрх алга."
}
StatusMeaning
400 / 422Invalid request data — a missing field or a wrong format.
401The key is missing, invalid or revoked.API_KEY_MISSINGAPI_KEY_INVALID
403A missing scope, a plan without API access or a suspended organization.SCOPE_MISSINGPLAN_FEATURE_UNAVAILABLETENANT_SUSPENDED
404Not found — another organization’s data is never visible either.
409A state conflict: not enough stock (OUT_OF_STOCK), the 24-hour window has closed (WINDOW_CLOSED) and so on.
429Rate limit exceeded — wait Retry-After seconds.RATE_LIMITED
5xxA temporary error — wait a moment and retry.

4Endpoints

All paths follow the base URL. :id is the resource UUID.

MethodPathScopeDescription
Contacts
GET/contactscontacts:readList contacts — filter by `q`, `tag`, `channelId`; paginated
GET/contacts/:idcontacts:readA contact card with conversation history
PATCH/contacts/:idcontacts:writeUpdate name, phone, email, tags, notes or broadcast opt-out
Conversations
GET/conversationsconversations:readList conversations — filter by `status`, `surface`, `channelId`, `q`; paginated
GET/conversations/:idconversations:readOne conversation — contact, channel and whether you can write (`window`)
GET/conversations/:id/messagesconversations:readMessages — newest first, `before` cursor, `limit` ≤ 100
POST/conversations/:id/messagesmessages:sendMessage a customer — 24-hour rules apply; the AI pauses in that conversation
Orders
GET/ordersorders:readList orders — filter by `status`, `paymentStatus`, `q`, `from`, `to`; paginated
POST/ordersorders:writeCreate an order — product `sku`/`productId` and quantity; priced from your catalog
GET/orders/:idorders:readOne order with its latest payment attempt
PATCH/orders/:idorders:writeChange the status or mark a payment manually (`PAID`, `REFUNDED`)
Products
GET/productsproducts:readList products — filter by `q` (Latin-letter aware), `category`, `active`; paginated
GET/products/:idproducts:readOne product
POST/productsproducts:writeAdd a product — SKUs are unique within your organization
PATCH/products/:idproducts:writeUpdate a product — price, stock, active
Appointments
GET/bookingsbookings:readList appointments — filter by `from`, `to`, `status`, `serviceId`; paginated
GET/bookings/:idbookings:readOne appointment
POST/bookingsbookings:writeBook an appointment — `409` when full; waits for payment when a deposit is due
GET/booking-servicesbookings:readActive services — duration, price, deposit, working hours
GET/booking-services/:id/slots?date=YYYY-MM-DDbookings:readFree slots of a service on a given day (in your organization’s time zone)

5Examples

Replace ak_live_… in the examples with your own key.

List orders

Newest first. Fetch the next page with page.

curl
curl https://aichat.mn/api/v1/orders?page=1&pageSize=20 \
  -H "Authorization: Bearer ak_live_…"

Create an order

Reference products by sku or productId — the server prices the order, delivery and total from your catalog. The response is the new order with its number and total.

curl
curl -X POST https://aichat.mn/api/v1/orders \
  -H "Authorization: Bearer ak_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "sku": "BAG-001",
        "quantity": 1
      }
    ],
    "customer": {
      "name": "Болор",
      "phone": "99112233",
      "address": "БЗД, 26-р хороо, 12-р байр"
    },
    "paymentMethod": "QPAY"
  }'

Message a customer

On Messenger, Instagram and WhatsApp you can only send within 24 hours of the customer’s last message — otherwise you get 409 WINDOW_CLOSED. The message shows up in the inbox like a team reply, and the AI pauses in that conversation.

curl
curl -X POST https://aichat.mn/api/v1/conversations/0c7d5a2e-8f1b-4c3d-9e6a-2b4f8d1c7a90/messages \
  -H "Authorization: Bearer ak_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Hi! Your order ships today."
  }'

Node.js

Keep the key in an environment variable and call the API from your server.

Node.js
const response = await fetch('https://aichat.mn/api/v1/orders?page=1&pageSize=20', {
  headers: { Authorization: `Bearer ${process.env.AICHAT_API_KEY}` }
})

if (!response.ok) throw new Error(`aichat.mn API ${response.status}`)

const { data, total } = await response.json()

6Webhooks

When something happens, we send a JSON POST to your HTTPS endpoint.

  1. 1

    Add an endpoint

    In “Settings → API & webhooks”, click “Add webhook”, enter your HTTPS URL and pick the events to receive (up to 10 endpoints).

  2. 2

    Store the secret

    The signing secret is shown only once. Use “Rotate secret” whenever you need a new one.

  3. 3

    Test it

    “Send test” delivers a ping event. Every delivery’s response and timing are kept in the delivery log.

Events

EventSent when
order.createdA new order is created in chat, by your team or through the API
order.updatedAn order’s status or details change
order.paidAn order is paid or a payment is confirmed manually
booking.createdA new appointment is booked
booking.updatedAn appointment’s status or time changes
contact.createdA new customer writes for the first time
conversation.handoffA conversation is handed off to your team
message.receivedA new message arrives from a customer

Headers

HeaderValue
X-Aichat-Signaturet=<unix seconds>,v1=<hex> — an HMAC-SHA256 signature
X-Aichat-EventThe event name — order.paid and so on
X-Aichat-DeliveryA unique delivery id — the same across retries

Body example — order.paid

data has the same shape as the resource in the API — here, an order.

JSON
{
  "id": "evt_5f0c2b9e1d473e8a",
  "event": "order.paid",
  "createdAt": "2026-11-04T09:34:52.000Z",
  "tenant": {
    "id": "6d2e8f4a-0b1c-4d3e-9f5a-7b8c9d0e1f2a",
    "slug": "nomin-stor"
  },
  "data": {
    "id": "9b2f6c1e-4d3a-4c55-9a43-1f2e3d4c5b6a",
    "number": 1024,
    "status": "CONFIRMED",
    "paymentStatus": "PAID",
    "paymentMethod": "QPAY",
    "items": [
      {
        "id": "3e8a1f20-7c41-4b9e-8d5a-6f0c2b9e1d47",
        "productId": "a1c4e7b2-5d8f-4a3b-9c6e-0f2d4b7a9e13",
        "sku": "BAG-001",
        "name": "Арьсан цүнх",
        "unitPrice": 89000,
        "quantity": 1,
        "lineTotal": 89000
      }
    ],
    "subtotal": 89000,
    "deliveryFee": 5000,
    "total": 94000,
    "customer": {
      "name": "Болор",
      "phone": "99112233",
      "address": "БЗД, 26-р хороо, 12-р байр"
    },
    "note": "",
    "source": "AI",
    "attribution": "AI",
    "attributedUser": null,
    "contact": {
      "id": "f4b8c2d6-1a3e-4f5b-8c7d-9e0a1b2c3d4e",
      "externalId": "7712093384521",
      "name": "Болор",
      "username": null,
      "avatarUrl": null
    },
    "conversationId": "0c7d5a2e-8f1b-4c3d-9e6a-2b4f8d1c7a90",
    "channel": {
      "id": "c2a7e9f1-3b5d-4c8e-a1f6-7d9b0e2c4a68",
      "type": "INSTAGRAM",
      "name": "nominstor",
      "username": "nominstor",
      "pictureUrl": null,
      "status": "ACTIVE"
    },
    "payment": {
      "id": "e7d1c3b5-9a2f-4e6d-8b0c-1f3a5e7c9b2d",
      "method": "QPAY",
      "amount": 94000,
      "status": "PAID",
      "shortUrl": "https://s.qpay.mn/…",
      "qrImage": null,
      "bankLinks": [],
      "reference": null,
      "expiresAt": "2026-11-05T09:30:12.000Z",
      "paidAt": "2026-11-04T09:34:51.000Z",
      "createdAt": "2026-11-04T09:30:12.000Z"
    },
    "createdBy": null,
    "createdAt": "2026-11-04T09:30:12.000Z",
    "updatedAt": "2026-11-04T09:34:51.000Z",
    "paidAt": "2026-11-04T09:34:51.000Z",
    "fulfilledAt": null,
    "cancelledAt": null
  }
}

7Verifying signatures

Use the signature to check that a request came from us and wasn’t changed on the way:

  1. Split the X-Aichat-Signature header on , to get t (unix seconds) and v1 (hex).
  2. Take the raw body bytes before parsing them and build the string t + . + body.
  3. Compute the HMAC-SHA256 of that string with your webhook secret, as hex.
  4. Compare it with v1 using a constant-time (timing-safe) comparison.
  5. Reject requests whose t is more than 300 seconds away from the current time — this stops replays.

Sample code

JavaScript · Express
import crypto from 'node:crypto'
import express from 'express'

const SECRET = process.env.AICHAT_WEBHOOK_SECRET
const TOLERANCE = 300 // seconds

const verify = (header, rawBody) => {
  const parts = Object.fromEntries(
    String(header ?? '')
      .split(',')
      .map(part => part.trim().split('='))
  )
  const timestamp = Number(parts.t)

  if (!parts.v1 || !Number.isInteger(timestamp)) return false
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE) return false

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${parts.t}.`)
    .update(rawBody)
    .digest()
  const received = Buffer.from(parts.v1, 'hex')

  return received.length === expected.length && crypto.timingSafeEqual(received, expected)
}

const app = express()

// Keep the raw body: the signature covers the exact bytes that were sent.
app.post('/webhooks/aichat', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verify(req.get('X-Aichat-Signature'), req.body)) return res.sendStatus(400)

  const event = JSON.parse(req.body.toString('utf8'))
  // Deduplicate by event.id, queue the work and answer quickly.
  res.sendStatus(200)
})

app.listen(3000)

8Delivery and retries

  • Answer 2xx as soon as you receive the request and do slow work afterwards — within 10 seconds.
  • If we don’t get a 2xx response, we retry up to 6 times — after 5 s, 30 s, 2 min, 10 min, 30 min, 1 h (7 attempts in total).
  • The same event can arrive more than once — deduplicate by the body’s id.
  • Order isn’t guaranteed — sort by createdAt.
  • Endpoints that fail 50 times in a row are disabled automatically — fix the problem and re-enable them in the dashboard.
  • Endpoints must be HTTPS and reachable from the internet — we never deliver to private network IPs.
  • The delivery log is kept for 30 days.

9Partner API (delivery)

A separate API for delivery companies. It doesn’t use an organization’s ak_live_… key but a dp_live_… key issued by the platform, and returns deliveries from every shop that chose that company.

  • Base URL: https://aichat.mn/api/partner/v1
  • Authentication: Authorization: Bearer dp_live_… — the platform shows the key only once
  • Rate limit: 300 requests per minute per partner
  • Webhooks: delivery.created, delivery.cancelled — same signature scheme as above
MethodPathDescription
GET/meYour company profile — status, number of connected shops and rate limit
GET/deliveriesDeliveries — filter by status, updatedSince (ISO date), paginate with page, pageSize
GET/deliveries/:idOne delivery — shop, pickup and drop-off, items, cash to collect, status history
POST/deliveries/:id/acceptAccept the delivery (PENDING → ACCEPTED)
POST/deliveries/:id/rejectReject — reason required (PENDING → REJECTED)
POST/deliveries/:id/statusUpdate status, courier, tracking, ETA and delivery fee

Status flow, webhooks, sample code and how to become a partner: For delivery companies

Ready to connect your systems?

The REST API and webhooks are included in the Pro plans. Questions? Email us.