Message24 answers customer DMs (Instagram, WhatsApp, Facebook, TikTok) with an AI agent. To answer "do you have this in red, size M, how much?" the agent searches a local copy of the shop's catalog. So we pull your products into our database and keep them fresh.
Read-only, inbound only — we never write to your system in this scope. We already do this for WooCommerce, ERPNext, ShopLink and one custom vendor API, so the shape below is what our importer already consumes.
GET /shop → which shop this key belongs to, and its currency
GET /products?page=&limit=&since= → the catalog
Both public HTTPS, both authenticated with a per-shop API key in a header (X-Api-Key: <key>), which the owner generates and revokes from your dashboard. That key plus your base URL is the whole credential we store, encrypted.
{ "id": "88", "name": "Zara Erbil", "currency": "IQD" }We show name back to the owner when they paste their key, so they can confirm they connected the right shop and their API key is valid. currency labels every price in the feed.
| Param | ||
|---|---|---|
page, limit |
pagination | Stable ordering — by id or created_at, not by anything that changes. Response returns totalPages or hasNextPage. |
since |
ISO 8601 | Only products whose updatedAt is newer, including deleted ones. This is how we stay current between full crawls. |
{
"products": [
{
"id": "1042",
"sku": "TS-001",
"price": 85.0,
"originalPrice": 100.0,
"discountEndsAt": "2026-09-01T00:00:00Z",
"inStock": true,
"isActive": true,
"updatedAt": "2026-08-01T10:32:00Z",
"deletedAt": null,
"category": "Clothing -> Men -> T-Shirts",
"brand": "Zara",
"translation": {
"en": { "name": "Cotton T-Shirt", "description": "100% cotton, regular fit" },
"ar": { "name": "تي شيرت قطن", "description": "قطن ١٠٠٪" },
"ku": { "name": "تیشێرتی لۆکە", "description": "١٠٠٪ لۆکە" }
},
"images": { "main": "https://cdn.../1.jpg", "other": ["https://cdn.../2.jpg"] },
"attributes": [{ "name": "Color", "values": ["Black", "White"] }],
"attributeImages": { "Black": ["https://cdn.../blk.jpg"] },
"variants": [
{ "id": "9001", "attributes": { "Color": "Black" }, "price": 85.0, "inStock": true, "sku": "TS-001-BLK" },
{ "id": "9002", "attributes": { "Color": "White" }, "price": 85.0, "inStock": false, "sku": "TS-001-WHT" }
]
}
],
"pagination": { "currentPage": 1, "totalPages": 7, "hasNextPage": true, "total": 634 }
}| Field | Required | Notes |
|---|---|---|
id |
yes | Stable and permanent — our upsert key. If it changes we create a duplicate. |
translation.*.name |
yes | en, ar, ku (Kurdish Sorani). Send what the shop filled in; missing languages are fine. |
price |
yes | Number, current selling price. No symbol, no formatting. |
inStock |
yes | Boolean, or a quantity we read as > 0. |
isActive |
yes | Published/enabled in the shop. Inactive → hidden from the agent, row kept. |
updatedAt |
yes | ISO 8601 with timezone. What since filters on. |
deletedAt |
yes | ISO 8601, null when live. |
sku |
strongly | Used in search, and quoted to the customer for follow-up. |
translation.*.description |
strongly | Plain text or HTML (we strip tags). This is what the AI reads to answer detail questions. |
images.main, images.other |
strongly | Absolute URLs. First 5 are used, in order. |
category |
strongly | Full path if you have a tree: "Clothing -> Men -> T-Shirts", or an array of ancestor names. |
brand |
strongly | Plain name. |
originalPrice, discountEndsAt |
if discounted | Pre-discount price, and when the discount stops (omit if open-ended). |
attributes, variants |
if applicable | See rule 6. |
attributeImages |
optional | Photos per attribute value, e.g. per colour. First 2 per value are used. |
barcode, videoUrl |
optional |
priceis already the selling price. If a discount is live,priceis the discounted figure andoriginalPriceis what it was. Never send a scheduled or expired discount — we can't repair a price that was wrong at import time.updatedAtchanges on every write — including a price or stock change made by a background job, a bulk import, or anything else your admin UI isn't involved in. If it doesn't,sincesilently skips the update and the AI quotes a stale price.- Deleted products still come back in a
sincewindow, carryingdeletedAt. A product that just disappears from an incremental response is invisible to us and we'd go on selling it forever. (If soft-delete doesn't fit your model:GET /products/deleted?since=returning ids.) - An empty list means "zero products", never a failure. We deactivate products that vanish from a full crawl, so a broken response returning
[]wipes a catalog. Errors must be HTTP errors. - Image URLs must be publicly fetchable — no auth, no signed expiry, stable over time. We download each one and re-host it on our S3, then send it to customers over WhatsApp and Instagram.
- Every variant carries the full attribute map, spelled and cased exactly as in
attributes. The agent quotes per-variantpriceandinStock. Omit both fields for products without variants. - Be consistent about the Kurdish key.
ku,ckb,kur— any is fine, tell us which. - Tell us your rate limit. We pull a whole catalog in one pass, a few requests per second.