1Benefits
Once a shop picks you as its delivery company, every order comes straight to you.
Orders arrive by themselves
Every order becomes a delivery for you right away — no forwarding by phone or chat.
Accurate details
Recipient name, phone, address, items and cash to collect — an exact copy of the order at the moment it was sent.
One API — every shop
No separate integration per shop: one key and one webhook for every shop that chose you.
Customers stay informed
Your status updates send “Out for delivery” and “Delivered” messages to the customer automatically and notify the shop.
2How it works
Four steps from connecting to your first delivery.
- 1
Apply
Email hello@aichat.mn with your company name, coverage area and pricing. We register you and give you an API key and a webhook secret — each shown only once.
- 2
Shops choose you
Shops pick you as their delivery company under “Products & orders → Settings”, where your coverage and pricing notes are shown.
- 3
Deliveries arrive
When a shop clicks “Send for delivery” on an order — or once it’s paid — a delivery is created and a
delivery.createdwebhook arrives. Without webhooks, pollGET /deliveries. - 4
Accept and update
Accept or reject, then send the courier, tracking and status (picked up → in transit → delivered) through the API.
3Status flow
Partners can only make the transitions below — anything else returns 422 (DELIVERY_INVALID_TRANSITION) with the allowed statuses in allowed. A delivery in a final status never changes again.
- Pending
PENDINGOrRejectedREJECTED - Accepted
ACCEPTEDOrFailedFAILED - Picked up
PICKED_UPOrFailedFAILEDcan go straight to “Delivered”
- In transit
IN_TRANSITOrFailedFAILED - Delivered
DELIVERED
4API overview
Send Authorization: Bearer dp_live_… with every request. The key only reaches deliveries sent to your company.
Base URL
https://aichat.mn/api/partner/v1| Method | Path | Description |
|---|---|---|
| GET | /me | Your company profile — status, number of connected shops and rate limit |
| GET | /deliveries | Deliveries — filter by status, updatedSince (ISO date), paginate with page, pageSize |
| GET | /deliveries/:id | One delivery — shop, pickup and drop-off, items, cash to collect, status history |
| POST | /deliveries/:id/accept | Accept the delivery (PENDING → ACCEPTED) |
| POST | /deliveries/:id/reject | Reject — reason required (PENDING → REJECTED) |
| POST | /deliveries/:id/status | Update status, courier, tracking, ETA and delivery fee |
Errors
| Status | Meaning |
|---|---|
| 401 | The key is missing (PARTNER_KEY_MISSING), wrong or has been replaced (PARTNER_KEY_INVALID). |
| 403 | The partner account is suspended (PARTNER_SUSPENDED). |
| 404 | Delivery not found (DELIVERY_NOT_FOUND) — deliveries sent to other companies look the same. |
| 409 | The delivery changed in the meantime (DELIVERY_CHANGED) — read it again and retry. |
| 422 | The status transition isn’t allowed (DELIVERY_INVALID_TRANSITION) — for example moving a delivered order back to “In transit” — or a field is invalid. |
| 429 | Rate limit exceeded (RATE_LIMITED) — wait Retry-After seconds. Repeated attempts with wrong keys temporarily throttle that IP. |
Examples
Pending deliveries
curl https://aichat.mn/api/partner/v1/deliveries?status=PENDING&pageSize=20 \
-H "Authorization: Bearer dp_live_…"Report that the courier picked it up
curl -X POST https://aichat.mn/api/partner/v1/deliveries/5d1c9e2a-7b3f-4a8e-9c6d-2f1e0b7a4c38/status \
-H "Authorization: Bearer dp_live_…" \
-H "Content-Type: application/json" \
-d '{
"status": "PICKED_UP",
"courierName": "Ганбаатар",
"courierPhone": "88112233",
"trackingCode": "SH-1024",
"trackingUrl": "https://shuurkhai.mn/track/SH-1024",
"etaAt": "2026-11-04T12:00:00.000Z",
"feeMnt": 5000
}'Reject
curl -X POST https://aichat.mn/api/partner/v1/deliveries/5d1c9e2a-7b3f-4a8e-9c6d-2f1e0b7a4c38/reject \
-H "Authorization: Bearer dp_live_…" \
-H "Content-Type: application/json" \
-d '{
"reason": "Хамрах бүсээс гадуур"
}'Node.js — polling without webhooks
const response = await fetch('https://aichat.mn/api/partner/v1/deliveries?status=PENDING&pageSize=50', {
headers: { Authorization: `Bearer ${process.env.AICHAT_PARTNER_KEY}` }
})
if (response.status === 429) {
// Rate limited: wait for Retry-After seconds, then try again.
}
const { data, total } = await response.json()Delivery body — GET /deliveries/:id
codAmount is the cash to collect on delivery (0 means paid). Totals and items come from the shop’s order — partners can’t change them.
{
"id": "5d1c9e2a-7b3f-4a8e-9c6d-2f1e0b7a4c38",
"status": "PENDING",
"orderNumber": 1024,
"shop": {
"slug": "nomin-stor",
"name": "Номин Стор",
"phone": "77001234",
"address": "СБД, 1-р хороо, Номин төв"
},
"pickup": {
"name": "Номин Стор",
"phone": "77001234",
"address": "СБД, 1-р хороо, Номин төв"
},
"dropoff": {
"name": "Болор",
"phone": "99112233",
"address": "БЗД, 26-р хороо, 12-р байр"
},
"items": [
{
"name": "Арьсан цүнх",
"sku": "BAG-001",
"quantity": 1
}
],
"codAmount": 0,
"orderTotal": 94000,
"note": "Оройн 18:00-аас хойш",
"trackingCode": null,
"trackingUrl": null,
"courierName": null,
"courierPhone": null,
"etaAt": null,
"feeMnt": null,
"events": [
{
"status": "PENDING",
"at": "2026-11-04T09:34:52.000Z",
"note": null,
"by": "system"
}
],
"createdAt": "2026-11-04T09:34:52.000Z",
"updatedAt": "2026-11-04T09:34:52.000Z"
}5Webhooks
New deliveries and cancellations are POSTed as JSON to your HTTPS URL right away. Signatures use the same scheme as developer webhooks.
| Event | When it’s sent |
|---|---|
delivery.created | When a shop sends you an order, or automatically once it’s paid |
delivery.cancelled | When a shop cancels an unfinished delivery |
Headers
| Header | Value |
|---|---|
X-Aichat-Signature | t=<unix seconds>,v1=<hex> — HMAC-SHA256 signature |
X-Aichat-Event | Event name — e.g. delivery.created |
X-Aichat-Delivery | Unique delivery id — unchanged across retries |
Example body — delivery.created
{
"id": "evt_7b3f4a8e9c6d2f1e0b7a4c38",
"event": "delivery.created",
"createdAt": "2026-11-04T09:34:53.000Z",
"data": {
"id": "5d1c9e2a-7b3f-4a8e-9c6d-2f1e0b7a4c38",
"status": "PENDING",
"orderNumber": 1024,
"shop": {
"slug": "nomin-stor",
"name": "Номин Стор",
"phone": "77001234",
"address": "СБД, 1-р хороо, Номин төв"
},
"pickup": {
"name": "Номин Стор",
"phone": "77001234",
"address": "СБД, 1-р хороо, Номин төв"
},
"dropoff": {
"name": "Болор",
"phone": "99112233",
"address": "БЗД, 26-р хороо, 12-р байр"
},
"items": [
{
"name": "Арьсан цүнх",
"sku": "BAG-001",
"quantity": 1
}
],
"codAmount": 0,
"orderTotal": 94000,
"note": "Оройн 18:00-аас хойш",
"trackingCode": null,
"trackingUrl": null,
"courierName": null,
"courierPhone": null,
"etaAt": null,
"feeMnt": null,
"events": [
{
"status": "PENDING",
"at": "2026-11-04T09:34:52.000Z",
"note": null,
"by": "system"
}
],
"createdAt": "2026-11-04T09:34:52.000Z",
"updatedAt": "2026-11-04T09:34:52.000Z"
}
}Verify the signature — Node.js
Before parsing, compute the HMAC-SHA256 of t + . + the raw body with your webhook secret and compare it to v1 in constant time. Reject requests whose t is more than 300 seconds away from now.
import crypto from 'node:crypto'
import express from 'express'
const SECRET = process.env.AICHAT_PARTNER_WEBHOOK_SECRET
const KEY = process.env.AICHAT_PARTNER_KEY
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-delivery', express.raw({ type: 'application/json' }), async (req, res) => {
if (!verify(req.get('X-Aichat-Signature'), req.body)) return res.sendStatus(400)
const event = JSON.parse(req.body.toString('utf8'))
res.sendStatus(200) // answer quickly, then do the work
if (event.event === 'delivery.created') {
// Deduplicate by event.id, then accept the delivery.
await fetch(`https://aichat.mn/api/partner/v1/deliveries/${event.data.id}/accept`, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}` }
})
}
})
app.listen(3000)6Become a partner
Run a delivery or courier service? Write to us. Connecting is free.
What to include
- Company name, registration number and a contact person
- Coverage area, delivery times and pricing
- An HTTPS URL for webhooks (optional)
- An email address for technical questions
API and webhooks · hello@aichat.mn