Skip to main content
aichat.mn

Delivery partners

Deliveries from many shops through one connection

Shops that sell in chat with aichat.mn send you their orders with one click — or automatically once paid. You receive every shop’s deliveries with a single API key and keep their status up to date.

  • Free to connect
  • Bearer dp_live_…
  • 300 requests per minute

Pending deliveries

curl
curl https://aichat.mn/api/partner/v1/deliveries?status=PENDING&pageSize=20 \
  -H "Authorization: Bearer dp_live_…"

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. 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. 2

    Shops choose you

    Shops pick you as their delivery company under “Products & orders → Settings”, where your coverage and pricing notes are shown.

  3. 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.created webhook arrives. Without webhooks, poll GET /deliveries.

  4. 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.

Delivery status flow: the main path with reject and failure branches
  1. PendingPENDING
    OrRejectedREJECTED
  2. AcceptedACCEPTED
    OrFailedFAILED
  3. Picked upPICKED_UP
    OrFailedFAILED

    can go straight to “Delivered”

  4. In transitIN_TRANSIT
    OrFailedFAILED
  5. DeliveredDELIVERED

4API overview

Send Authorization: Bearer dp_live_… with every request. The key only reaches deliveries sent to your company.

Base URL

URL
https://aichat.mn/api/partner/v1
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

Errors

StatusMeaning
401The key is missing (PARTNER_KEY_MISSING), wrong or has been replaced (PARTNER_KEY_INVALID).
403The partner account is suspended (PARTNER_SUSPENDED).
404Delivery not found (DELIVERY_NOT_FOUND) — deliveries sent to other companies look the same.
409The delivery changed in the meantime (DELIVERY_CHANGED) — read it again and retry.
422The status transition isn’t allowed (DELIVERY_INVALID_TRANSITION) — for example moving a delivered order back to “In transit” — or a field is invalid.
429Rate limit exceeded (RATE_LIMITED) — wait Retry-After seconds. Repeated attempts with wrong keys temporarily throttle that IP.

Examples

Pending deliveries

curl
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
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
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

Node.js
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.

JSON
{
  "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.

EventWhen it’s sent
delivery.createdWhen a shop sends you an order, or automatically once it’s paid
delivery.cancelledWhen a shop cancels an unfinished delivery

Headers

HeaderValue
X-Aichat-Signaturet=<unix seconds>,v1=<hex> — HMAC-SHA256 signature
X-Aichat-EventEvent name — e.g. delivery.created
X-Aichat-DeliveryUnique delivery id — unchanged across retries

Example body — delivery.created

JSON
{
  "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.

JavaScript · Express
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
Send an application

API and webhooks · hello@aichat.mn