Custom API contract · version 2

Let the assistant use your system

Implement a few HTTPS routes under a base URL of your choosing and connect it in the dashboard. Seler calls them live while a customer is chatting — to quote a price, place an order, read the customer's account, book a time — and never stores what they return. JSON in, JSON out, UTF-8.

Four halves, pick what you have

The contract is in four capabilities. A store implements the first two; a clinic might implement only customers and appointments. Your /verify answer says which you implement, and the assistant is given tools for exactly those.

catalogsearchProducts, getProductThe assistant quotes prices and availability and sends photos.
ordersplaceOrder, getOrderStatus, cancelOrderCash-on-delivery orders, with the customer's explicit confirmation.
customerslookupCustomerRecord, and lead pushBalance, points, history — whatever you expose; and every lead the assistant captures lands in your CRM.
appointmentslistAppointmentSlots, bookAppointmentReal slots from your calendar, booked with the customer's explicit confirmation.

Requests from Seler

Headers on every request
Authorization: Bearer <secret from the dashboard>
Accept: application/json
X-Seler-Contract: 2
Content-Type: application/json        (on POST)
Idempotency-Key: <key>                (on order and appointment creation)
  • Answer 401 when the bearer token is not the secret the owner gave you.
  • The base URL must be https:// on a public host. Private addresses, localhost and plain HTTP are refused.
  • Answer within 10 seconds. Responses larger than 5 MB are dropped.
  • Redirects are followed (at most 3), but answer directly where you can.
  • Paths are appended to your base URL: with a base of https://shop.example.com/seler, verification is GET https://shop.example.com/seler/verify.

Routes

MethodPathCapabilityPurpose
GET/verifycatalogProve the secret; name, currency, capabilities
GET/productscatalogSearch the catalogue
GET/products/{id}catalogOne product, with variants
POST/ordersordersPlace an order (idempotent)
GET/orders/{reference}ordersAn order by number or id
POST/orders/{reference}/cancelordersCancel an order
GET/customers/lookupcustomersThe record behind a verified phone number
POST/customerscustomersA lead the assistant captured
GET/appointments/slotsappointmentsFree slots
POST/appointmentsappointmentsBook a slot (idempotent)

Verify

Called when the owner presses connect, and again whenever Seler re-checks the connection. Return the display name, the currency as an ISO 4217 code, and the capabilities you implement.

GET /verify → 200
{ "name": "Aroma Baghdad", "currency": "IQD", "capabilities": ["catalog", "orders", "customers"] }

catalog

Search products

q is what the customer asked for, in their own words, often Arabic. Search however your catalogue searches best; Seler re-ranks what you return. limit is a hint (at most 50). category is optional. Return only products a customer may buy now or hear about.

GET /products?q=عباية&limit=20 → 200
{
  "products": [
    {
      "id": "42",
      "title": "عباية سوداء كلاسيك",
      "description": "قماش كريب، مقاسات 52 إلى 60.",
      "price": 45000,
      "compareAtPrice": 60000,
      "currency": "IQD",
      "imageUrl": "https://shop.example.com/img/42.jpg",
      "url": "https://shop.example.com/p/42",
      "categories": ["عبايات"],
      "inStock": true,
      "quantity": 12,
      "variants": [
        { "id": "42-54", "title": "54", "price": 45000, "inStock": true,  "quantity": 3, "options": { "المقاس": "54" } },
        { "id": "42-56", "title": "56", "price": 45000, "inStock": false, "options": { "المقاس": "56" } }
      ]
    }
  ]
}

One product

GET /products/{id} → 200, or 404 when there is no such product
{ "id": "42", "title": "عباية سوداء كلاسيك", "price": 45000, "inStock": true, "variants": [] }

The product shape

idstring or numberRequired. Stable; it comes back in orders as variantId when there are no variants.
titlestringRequired.
pricenumberRequired. In the store currency, no formatting.
inStockbooleanRequired. Whether it can be sold right now.
quantityinteger or nullOptional. Never shown to customers; below 4 becomes "few left". Omit or null when you do not track stock.
descriptionstring or nullOptional. Plain text; the assistant reads the first 400 characters.
compareAtPricenumber or nullOptional. The old price, shown as a discount only when higher than price.
currencystringOptional. Defaults to the currency from /verify.
imageUrlURL or nullOptional. Sent to the customer as a photo when available.
urlURL or nullOptional. A link the customer can open.
categoriesstring[]Optional.
variantsVariant[]Optional. Each has id, title, price, inStock, and optionally quantity and options (a map of option name to value). Orders reference a variant id when variants exist.

orders

Create an order

Cash on delivery, the way the assistant sells. Make this idempotent: if you have already seen idempotencyKey, return the order you created then, with 200, and do not create another. A retry after a timeout must never charge a customer twice.

POST /orders
{
  "idempotencyKey": "seler-33333333-…-a1b2c3d4",
  "lines": [ { "variantId": "42-54", "quantity": 1 } ],
  "customer": { "name": "زينب الحسيني", "phone": "+9647701234567", "email": null },
  "address": { "line1": "حي الجامعة، شارع 12", "city": "بغداد", "province": "بغداد", "country": "IQ", "notes": null },
  "note": "الاتصال قبل التوصيل",
  "source": "seler"
}
→ 201 (or 200 for a repeated key)
{
  "id": "8812",
  "number": "1001",
  "status": "confirmed",
  "total": 45000,
  "currency": "IQD",
  "placedAt": "2026-09-07T14:03:00Z",
  "trackingUrl": null,
  "customerPhone": "+9647701234567",
  "lines": [ { "title": "عباية سوداء كلاسيك — 54", "quantity": 1, "price": 45000 } ]
}

Look an order up

The reference is the customer-facing number or your id, without a leading #. Always return customerPhone: Seler shows an order only to the person whose verified number it was placed under, because order numbers are printed on receipts and easy to guess.

GET /orders/{reference} → 200, or 404
{ "id": "8812", "number": "1001", "status": "shipped", "total": 45000, "customerPhone": "+9647701234567", "trackingUrl": "https://courier.example/track/ABC", "lines": [] }

Cancel an order

POST /orders/{reference}/cancel → 200 with the updated order
{ "reason": "العميل غيّر رأيه" }

Answer 409 with a message when the order can no longer be cancelled; the assistant tells the customer to contact the team.

The order shape

idstring or numberRequired. Your internal id.
numberstring or numberRequired. What the customer sees on the receipt.
statusstringRequired. pending, confirmed, processing, shipped, delivered, cancelled, refunded — or the synonyms new, preparing, out_for_delivery, completed, canceled. Anything else is "unknown".
totalnumberRequired.
customerPhonestring or nullRequired for lookups to work. The number the order was placed under.
currencystringOptional. Defaults to the store currency.
placedAtISO 8601 or nullOptional.
trackingUrlURL or nullOptional. Sent to the customer when present.
lines{ title, quantity, price }[]Optional. What was ordered, as titles.

customers

Look a customer up

Called when the customer asks about their account, their history, their balance — anything the team would look up by phone. The number is always the one Seler verified on the conversation, never one the customer typed into the chat, so your system may trust it the way it trusts a caller who has proved their number.

GET /customers/lookup?phone=%2B9647701234567 → 200
{
  "customer": {
    "id": "C-1042",
    "name": "زينب الحسيني",
    "fields": {
      "الرصيد": "125,000 د.ع",
      "نقاط الولاء": 340,
      "آخر زيارة": "2026-08-30",
      "عضوية": "ذهبية"
    }
  }
}
→ 200 when you have no record
{ "customer": null }
idstring or number or nullOptional. Your id.
namestring or nullOptional.
fieldsobjectLabel → value. Strings (≤ 300 chars), numbers, booleans or null; up to 30. The assistant repeats these to the customer, so include only what the customer themselves may hear. No internal notes, no margins.

Receive a lead

Whenever the assistant writes down someone to follow up with — a quote, a callback, a viewing — it also sends the lead here, once, best effort. Create or update the record; answer with its id.

POST /customers
{
  "name": "زينب الحسيني",
  "phone": "+9647701234567",
  "note": "تريد عرض سعر لتجهيز مطبخ كامل",
  "conversationId": "33333333-…",
  "source": "seler"
}
→ 200 or 201
{ "customer": { "id": "C-1042" } }

appointments

Free slots

Called before the assistant offers a time. It never invents one. from and to are ISO dates or datetimes as the customer asked (“Sunday morning” becomes a date); service is what they named, if anything. Return up to 30 slots and, if useful, the services you offer so the assistant can ask which.

GET /appointments/slots?from=2026-09-16&to=2026-09-17&service=تنظيف → 200
{
  "services": ["تنظيف أسنان", "فحص", "تبييض"],
  "slots": [
    { "id": "s-2026-09-16-0930", "startsAt": "2026-09-16T09:30:00+03:00", "endsAt": "2026-09-16T10:00:00+03:00", "service": "تنظيف أسنان" },
    { "id": "s-2026-09-16-1100", "startsAt": "2026-09-16T11:00:00+03:00", "endsAt": "2026-09-16T11:30:00+03:00", "service": "تنظيف أسنان" }
  ]
}

Book a slot

Called only after the customer has seen the service, the date and the time and said yes. Idempotent on the key, like orders: a retry returns the appointment already made.

POST /appointments
{
  "idempotencyKey": "seler-33333333-…-s-2026-09-16-0930",
  "slotId": "s-2026-09-16-0930",
  "service": "تنظيف أسنان",
  "customer": { "name": "زينب الحسيني", "phone": "+9647701234567" },
  "note": null,
  "source": "seler"
}
→ 201 (or 200 for a repeated key)
{ "appointment": { "id": "apt-5521", "startsAt": "2026-09-16T09:30:00+03:00", "endsAt": "2026-09-16T10:00:00+03:00", "service": "تنظيف أسنان", "status": "confirmed" } }

Answer 409 with a message when the slot was taken in the meantime; the assistant offers the customer another one.

Errors

Use HTTP status codes; a JSON body with a message is welcome and is shown to the team in the call log (never to customers).

401 / 403Wrong secretThe connection is marked as needing reconnection.
404No such product, order or recordThe assistant says it could not find it.
409 / 422Cannot do thatOut of stock, cannot cancel, slot taken, invalid line. The message helps the team.
429Slow downSeler backs off and retries later.
5xxYour side is downMarked as temporary; the connection is retried, not dropped.

Test it before connecting

curl
curl -sS https://shop.example.com/seler/verify \
  -H "Authorization: Bearer <secret>" -H "X-Seler-Contract: 2"

curl -sS "https://shop.example.com/seler/products?q=test&limit=5" \
  -H "Authorization: Bearer <secret>" -H "X-Seler-Contract: 2"

Checklist

  • HTTPS on a public host; the secret checked on every route.
  • /verify returns name, currency and capabilities.
  • Every product has id, title, price and inStock.
  • Order and appointment creation are idempotent on the key.
  • Orders return customerPhone in the same format they were placed with.
  • fields on a customer record hold only what the customer may hear.
  • Unknown routes and wrong methods answer 404, not a redirect to a web page.