{
  "info": {
    "_postman_id": "9b1f4c2a-5d30-4e71-8a62-0f7c1d9ab401",
    "name": "Shamz Public API",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "description": "Everything shamz.ai publishes for external use, as documented at https://shamz.ai/developers-guide\n\nTwo surfaces, two kinds of credential:\n\n- **Catalogue Push** — a merchant's own software sends us their catalogue. Authenticated with a bearer key the merchant issues from their dashboard (E-commerce → Push API) or the shamz app (Shop → API keys).\n- **Widget Chat** — send a message to a deployed assistant. Authenticated with the widget's `X-API-KEY`.\n\n## Before you run anything\n\nSet these in an environment or in the collection's Variables tab:\n\n| variable | where it comes from |\n|---|---|\n| `baseUrl` | `https://api.shamz.ai` (already set) |\n| `partnerKey` | the merchant's Push API key, shown once on creation |\n| `widgetApiKey` | the skill's widget API key |\n\nThe secret half of a Push API key is stored hashed and cannot be recovered. If it is lost, create another key and switch the old one off — switching off takes effect on the very next request.\n\n## What the tests assert\n\nEach request carries tests that check the published contract, not just \"did it 2xx\". Run the folder with the Collection Runner to confirm an integration end to end."
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.shamz.ai",
      "type": "string"
    },
    {
      "key": "partnerKey",
      "value": "",
      "type": "string"
    },
    {
      "key": "widgetApiKey",
      "value": "",
      "type": "string"
    },
    {
      "key": "sessionId",
      "value": "",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "Catalogue Push",
      "description": "For shops whose own software sends us the catalogue rather than letting us read it.\n\nThe assistant answers from the last thing you sent, and says when it was sent. Push cadence is yours to own — there is no call back to your system.\n\n**Order matters on a first run:** push the catalogue before sending updates, because an update for a product we have never seen is reported back in `unknown` rather than created.",
      "auth": {
        "type": "bearer",
        "bearer": [
          {
            "key": "token",
            "value": "{{partnerKey}}",
            "type": "string"
          }
        ]
      },
      "item": [
        {
          "name": "1. Push the full catalogue",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('202 Accepted — rows are saved, images follow', () => {",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "",
                  "const b = pm.response.json();",
                  "",
                  "pm.test('reports what it accepted', () => {",
                  "  pm.expect(b).to.have.property('accepted');",
                  "  pm.expect(b).to.have.property('created');",
                  "  pm.expect(b).to.have.property('updated');",
                  "  pm.expect(b).to.have.property('deactivated');",
                  "});",
                  "",
                  "pm.test('one bad row never costs the rest of the catalogue', () => {",
                  "  pm.expect(b.rejected).to.be.an('array');",
                  "});",
                  "",
                  "// Not a failure. A product with no image cannot appear in a WhatsApp",
                  "// carousel, and this is where you find that out rather than from a",
                  "// customer. Same for a missing url.",
                  "if (b.warnings && Object.keys(b.warnings).length) {",
                  "  console.log('warnings (these products will not carousel):', b.warnings);",
                  "}",
                  "",
                  "pm.test('tells you how many products can carousel', () => {",
                  "  pm.expect(b).to.have.property('carousel_ready');",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/partners/v1/catalog",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "partners",
                "v1",
                "catalog"
              ]
            },
            "description": "**This is the whole catalogue, not a page of it.** Any product we hold that this payload omits is deactivated — the row survives so past orders still resolve, but the assistant stops offering it. Send a partial list here and you will take the rest of the shop off sale.\n\nAnswers **202**, not 200: the product rows are committed before we answer, but images are fetched afterwards. `GET /status` reports how that went a few seconds later.\n\n### Per item\n\n| field | required | notes |\n|---|---|---|\n| `external_id` | yes | your own id. It is how we match an update to a product, so it must be stable. |\n| `name` | yes | |\n| `price` | yes | a number, not a string |\n| `currency` | yes | ISO code. A wrong code is rejected rather than guessed — it would otherwise reprice the shop into another country. |\n| `url` | strongly recommended | **no url, no carousel** |\n| `images` | strongly recommended | **no image, no carousel.** `https` only; we re-host them, so your CDN is not hit by shoppers. |\n| `stock_qty` | no | exact figures, answered as of the last push |\n| `specs` / `spec_text` | no | goes to the assistant's knowledge, so it can answer \"does it support eSIM\" |\n| `sequence` | no | any number that goes up. Only ever skips an **update** — a full catalogue is your current truth and is always applied. |\n\n`shop.knowledge` carries shop-wide answers — delivery, returns, warranty. Matching questions are replaced, not duplicated.\n\nLimits: 8 MB and 2,000 items. One full catalogue every 5 minutes.\n\n> The two example phones use placeholder photos from a public image service so that your very first run goes green and proves the whole pipeline — including us re-hosting each photo. Replace the whole `items` array with your real export; the photo URLs must be `https` and reachable without a login.",
            "body": {
              "mode": "raw",
              "options": {
                "raw": {
                  "language": "json"
                }
              },
              "raw": "{\n  \"shop\": {\n    \"knowledge\": [\n      {\n        \"question\": \"Do you deliver across Kuwait?\",\n        \"answer\": \"Yes, within 24 hours. Orders over KWD 50 ship free.\"\n      },\n      {\n        \"question\": \"What is the warranty?\",\n        \"answer\": \"One year from the local agent on all phones.\"\n      }\n    ]\n  },\n  \"items\": [\n    {\n      \"external_id\": \"SKU-1021\",\n      \"name\": \"iPhone 16 Pro 256GB\",\n      \"description\": \"Titanium finish, A18 Pro chip.\",\n      \"price\": 389.5,\n      \"currency\": \"KWD\",\n      \"stock_qty\": 4,\n      \"url\": \"https://example-shop.com/p/iphone-16-pro\",\n      \"images\": [\"https://picsum.photos/seed/shamz-ip16pro/800/800\"],\n      \"specs\": {\n        \"RAM\": \"8GB\",\n        \"Storage\": \"256GB\",\n        \"Warranty\": \"1 year\",\n        \"eSIM\": \"Supported\"\n      },\n      \"spec_text\": \"Dual eSIM, no physical SIM tray on GCC units.\",\n      \"sequence\": {{$timestamp}}\n    },\n    {\n      \"external_id\": \"SKU-1044\",\n      \"name\": \"Samsung Galaxy S25 Ultra 512GB\",\n      \"price\": 445.0,\n      \"currency\": \"KWD\",\n      \"stock_qty\": 0,\n      \"url\": \"https://example-shop.com/p/galaxy-s25-ultra\",\n      \"images\": [\"https://picsum.photos/seed/shamz-s25u/800/800\"],\n      \"specs\": { \"RAM\": \"12GB\", \"Storage\": \"512GB\" },\n      \"sequence\": {{$timestamp}}\n    }\n  ]\n}"
            }
          },
          "response": [
            {
              "name": "202 — accepted, with one row rejected and one warned",
              "status": "Accepted",
              "code": 202,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"accepted\": 412,\n  \"created\": 18,\n  \"updated\": 394,\n  \"deactivated\": 3,\n  \"rejected\": [\n    { \"external_id\": \"SKU-9\", \"error\": \"unknown currency 'XY'\" }\n  ],\n  \"warnings\": { \"SKU-77\": [\"no_image\"] },\n  \"carousel_ready\": 402,\n  \"images_pending\": 18\n}"
            }
          ]
        },
        {
          "name": "2. Update prices and stock",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "",
                  "const b = pm.response.json();",
                  "",
                  "pm.test('an update for a product we do not hold is reported, not invented', () => {",
                  "  pm.expect(b.unknown).to.be.an('array');",
                  "  if (b.unknown.length) {",
                  "    console.log('not in the catalogue yet — push the full catalogue first:', b.unknown);",
                  "  }",
                  "});",
                  "",
                  "pm.test('a stale retry is reported rather than applied', () => {",
                  "  const stale = Object.entries(b.warnings || {})",
                  "    .filter(([, w]) => w.includes('stale_ignored'));",
                  "  if (stale.length) {",
                  "    console.log('ignored as older than what we hold:', stale.map(([k]) => k));",
                  "  }",
                  "  pm.expect(b).to.have.property('accepted');",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/partners/v1/items",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "partners",
                "v1",
                "items"
              ]
            },
            "description": "Send as often as you like — this is the cheap one.\n\n**Values are absolute, not adjustments.** `stock_qty: 2` means two left, not two fewer.\n\n**Nothing is ever taken off sale here.** A product you omit is untouched; only a full catalogue retires anything. A withdrawn product therefore stays on sale until your next full push, so send one on a regular cadence.\n\n`sequence` matters on this endpoint. If your retry arrives after a newer update, we use it to avoid putting an old price back — a replayed absolute value is harmless, but a *late* one would roll the price back and no idempotency key can tell the two apart. Any number that goes up works. Unix **seconds** (`1792008852`) or **milliseconds** (`1792008852038`) are both fine — we store it as a 64-bit integer, so `Date.now()` and `time()` are equally safe. It only ever skips an *update*; a full catalogue is always applied.\n\nLimits: 1 MB and 1,000 items, 60 requests a minute.",
            "body": {
              "mode": "raw",
              "options": {
                "raw": {
                  "language": "json"
                }
              },
              "raw": "{\n  \"items\": [\n    { \"external_id\": \"SKU-1021\", \"stock_qty\": 2, \"sequence\": {{$timestamp}} },\n    { \"external_id\": \"SKU-1044\", \"price\": 429.0, \"in_stock\": false, \"sequence\": {{$timestamp}} }\n  ]\n}"
            }
          },
          "response": [
            {
              "name": "200 — one applied, one ignored as stale",
              "status": "OK",
              "code": 200,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"accepted\": 1,\n  \"rejected\": [],\n  \"warnings\": { \"SKU-1044\": [\"stale_ignored\"] },\n  \"unknown\": []\n}"
            }
          ]
        },
        {
          "name": "3. Check what we hold",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "",
                  "const b = pm.response.json();",
                  "",
                  "pm.test('reports the catalogue we are serving', () => {",
                  "  pm.expect(b).to.have.property('items_active');",
                  "  pm.expect(b).to.have.property('carousel_ready');",
                  "  pm.expect(b).to.have.property('last_full_push_at');",
                  "});",
                  "",
                  "// This is where an image failure surfaces. Images are fetched AFTER the",
                  "// push answers, so `images_at` says which push this outcome belongs to —",
                  "// read it before trusting the counts against a push you just made.",
                  "const run = b.last_run || {};",
                  "if (run.image_errors && Object.keys(run.image_errors).length) {",
                  "  console.log(`images we could not fetch (as of ${run.images_at}):`, run.image_errors);",
                  "}",
                  "",
                  "pm.test('products without a photo are visible to you, not just to us', () => {",
                  "  if (b.items_active > 0) {",
                  "    pm.expect(b.carousel_ready).to.be.at.most(b.items_active);",
                  "  }",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/partners/v1/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "partners",
                "v1",
                "status"
              ]
            },
            "description": "How many products are live, how many can appear in a WhatsApp carousel, and when you last sent us anything. Use it to check your own integration without asking us.\n\n`last_run` carries the outcome of the most recent push. Its `images_stored`, `images_skipped` and `image_errors` fields appear a few seconds after the push itself, because images are fetched after we answer — **this is where to look when a product arrived but its photo did not.** `image_errors` is keyed by your own `external_id` and capped at 25 entries.\n\nLimit: 120 requests a minute."
          },
          "response": [
            {
              "name": "200 — with the last run's outcome",
              "status": "OK",
              "code": 200,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"items_active\": 412,\n  \"items_inactive\": 3,\n  \"carousel_ready\": 402,\n  \"last_full_push_at\": \"2026-01-14T15:17:02Z\",\n  \"last_update_at\": \"2026-01-14T15:41:20Z\",\n  \"last_run\": {\n    \"kind\": \"snapshot\",\n    \"at\": \"2026-01-14T15:17:02Z\",\n    \"accepted\": 412,\n    \"created\": 18,\n    \"updated\": 394,\n    \"deactivated\": 3,\n    \"rejected\": [{ \"external_id\": \"SKU-9\", \"error\": \"unknown currency 'XY'\" }],\n    \"warnings\": { \"SKU-77\": [\"no_image\"] },\n    \"images_stored\": 394,\n    \"images_skipped\": 18,\n    \"image_errors\": { \"SKU-77\": \"HTTP 404\" }\n  }\n}"
            }
          ]
        },
        {
          "name": "4. Send a cart (optional — abandoned-cart follow-ups)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "",
                  "const b = pm.response.json();",
                  "",
                  "// The merchant has to switch cart follow-ups on. Until they do, this",
                  "// endpoint answers normally and stores nothing — so check this before",
                  "// concluding your integration works.",
                  "pm.test('cart follow-ups are switched on for this shop', () => {",
                  "  if (b.recovery_enabled === false) {",
                  "    console.log(b.note || 'Cart recovery is off — ask the merchant to enable it.');",
                  "  }",
                  "  pm.expect(b.recovery_enabled, 'the merchant has not enabled cart follow-ups yet').to.eql(true);",
                  "});",
                  "",
                  "pm.test('the cart was stored', () => {",
                  "  if (b.rejected && b.rejected.length) {",
                  "    console.log('REJECTED:', JSON.stringify(b.rejected));",
                  "  }",
                  "  pm.expect(b.rejected).to.be.an('array').that.is.empty;",
                  "  pm.expect(b.accepted).to.be.above(0);",
                  "});",
                  "",
                  "if (b.warnings && Object.keys(b.warnings).length) {",
                  "  console.log('warnings:', JSON.stringify(b.warnings));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/partners/v1/carts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "partners",
                "v1",
                "carts"
              ]
            },
            "description": "The current contents of a shopping cart. Send it whenever the cart changes — the contents you send replace what we held, so a retry is harmless and there is no need to send \"added one of these\".\n\n**Upsert only.** A cart you leave out of this call is untouched. This is the opposite of `PUT /catalog`, deliberately: carts appear and vanish constantly, and \"anything omitted is gone\" would wipe a live basket every time you sent one update.\n\n### Two required fields, and why\n\n- `recovery_url` — the reminder **is** a link back to the cart. Without it the message could only tell a shopper to go and find their own basket.\n- `customer_phone` **or** `customer_email` — without one there is nobody to remind.\n\nA cart missing either is rejected rather than stored, so you never accumulate carts that look healthy in `/status` and can never send anything.\n\n`discount_code` is optional and is **your** code, honoured by **your** checkout. We never mint one for a pushed shop — a code from our side would not exist at your checkout, and handing a shopper a discount that fails is worse than offering none.\n\n300 requests a minute. The merchant must switch cart follow-ups on before anything is stored; until then this answers normally and tells you so.",
            "body": {
              "mode": "raw",
              "options": {
                "raw": {
                  "language": "json"
                }
              },
              "raw": "{\n  \"carts\": [\n    {\n      \"external_id\": \"CART-9911\",\n      \"recovery_url\": \"https://linephones.com/cart/9911\",\n      \"customer_phone\": \"+96555512345\",\n      \"total_amount\": 429.5,\n      \"currency\": \"KWD\",\n      \"items\": [\n        {\n          \"external_id\": \"LP-IP16PM-256\",\n          \"name\": \"iPhone 16 Pro Max 256GB\",\n          \"quantity\": 1,\n          \"price\": 429.5\n        }\n      ]\n    }\n  ]\n}"
            }
          },
          "response": [
            {
              "name": "200 — stored",
              "status": "OK",
              "code": 200,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"accepted\": 1,\n  \"rejected\": [],\n  \"warnings\": {},\n  \"created\": 1,\n  \"updated\": 0,\n  \"recovery_enabled\": true\n}"
            },
            {
              "name": "200 — but follow-ups are switched off, so nothing was stored",
              "status": "OK",
              "code": 200,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"accepted\": 0,\n  \"rejected\": [],\n  \"warnings\": {},\n  \"created\": 0,\n  \"updated\": 0,\n  \"recovery_enabled\": false,\n  \"note\": \"Cart recovery is switched off for this shop, so nothing was stored. The merchant enables it in their shamz dashboard under the shop's Skill settings.\"\n}"
            }
          ]
        },
        {
          "name": "5. Send a placed order (optional)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "",
                  "const b = pm.response.json();",
                  "",
                  "pm.test('the order was accepted', () => {",
                  "  if (b.rejected && b.rejected.length) console.log('REJECTED:', JSON.stringify(b.rejected));",
                  "  pm.expect(b.rejected).to.be.an('array').that.is.empty;",
                  "});",
                  "",
                  "// Not a failure. Most orders are not recovered carts — this is reported",
                  "// so that when you DO expect a match and do not get one, you can see it.",
                  "if (b.no_cart_matched && b.no_cart_matched.length) {",
                  "  console.log('no cart matched these orders:', b.no_cart_matched);",
                  "}",
                  "console.log(`carts closed by this call: ${b.carts_closed}`);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/partners/v1/orders",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "partners",
                "v1",
                "orders"
              ]
            },
            "description": "An order was placed.\n\nTwo jobs:\n\n1. It stops a shopper who has already bought from being chased about the cart they bought from.\n2. It makes the follow-ups measurable. The cart records which reminder was the last one sent before the order arrived, so the merchant sees which nudge earned the sale instead of guessing.\n\nSend **every** order, not only ones you think came from a cart. Most will not match, and the response lists those in `no_cart_matched` — that is information, not an error.\n\n`cart_external_id` is the surest match. Without it we fall back to the phone or email, which still works but can only match a cart from the same shopper.\n\n300 requests a minute.",
            "body": {
              "mode": "raw",
              "options": {
                "raw": {
                  "language": "json"
                }
              },
              "raw": "{\n  \"orders\": [\n    {\n      \"external_id\": \"SO-55021\",\n      \"cart_external_id\": \"CART-9911\",\n      \"order_number\": \"55021\",\n      \"customer_phone\": \"+96555512345\",\n      \"total_amount\": 429.5,\n      \"currency\": \"KWD\"\n    }\n  ]\n}"
            }
          },
          "response": [
            {
              "name": "200 — order recorded and the cart it came from closed",
              "status": "OK",
              "code": 200,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"accepted\": 1,\n  \"rejected\": [],\n  \"warnings\": {},\n  \"carts_closed\": 1,\n  \"no_cart_matched\": []\n}"
            }
          ]
        },
        {
          "name": "Error contract",
          "description": "Worth running once, so your error handling is built against what we actually send rather than against a guess.",
          "item": [
            {
              "name": "Unknown currency — the row is rejected, the push is not",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "pm.test('the good row still lands', () => {",
                      "  pm.response.to.have.status(202);",
                      "  pm.expect(pm.response.json().accepted).to.be.above(0);",
                      "});",
                      "",
                      "pm.test('the bad row says why, by external_id', () => {",
                      "  const r = pm.response.json().rejected;",
                      "  pm.expect(r).to.be.an('array').that.is.not.empty;",
                      "  pm.expect(r[0]).to.have.property('external_id');",
                      "  pm.expect(r[0].error).to.include('currency');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "method": "PUT",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/partners/v1/catalog",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "partners",
                    "v1",
                    "catalog"
                  ]
                },
                "description": "A Pydantic-style validation dump is unreadable to an integrator, so the contract is per row: `{accepted, rejected: [{external_id, error}], warnings}`.\n\nNote this still deactivates products the payload omits — it is a full catalogue like any other.",
                "body": {
                  "mode": "raw",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  },
                  "raw": "{\n  \"items\": [\n    {\n      \"external_id\": \"SKU-1021\",\n      \"name\": \"iPhone 16 Pro 256GB\",\n      \"price\": 389.5,\n      \"currency\": \"KWD\",\n      \"url\": \"https://example-shop.com/p/iphone-16-pro\",\n      \"images\": [\"https://picsum.photos/seed/shamz-ip16pro/800/800\"]\n    },\n    {\n      \"external_id\": \"SKU-BAD\",\n      \"name\": \"Priced in a currency we do not know\",\n      \"price\": 10.0,\n      \"currency\": \"XYZ\"\n    }\n  ]\n}"
                }
              }
            },
            {
              "name": "Bad key — 401, and it never says which part was wrong",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "pm.test('401 Unauthorized', () => pm.response.to.have.status(401));",
                      "",
                      "pm.test('the body is identical for every failure', () => {",
                      "  // Unknown key, wrong secret, revoked key and a malformed token all",
                      "  // return exactly this. Telling a caller which one it was would tell",
                      "  // them whether a key exists.",
                      "  pm.expect(pm.response.json().detail).to.eql('Invalid API key.');",
                      "});"
                    ]
                  }
                }
              ],
              "request": {
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "shamz_live_0000000000000000_notarealsecretnotarealsecretnotareal",
                      "type": "string"
                    }
                  ]
                },
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/api/partners/v1/status",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "partners",
                    "v1",
                    "status"
                  ]
                },
                "description": "Always use `https://` directly. An `http://` request is redirected rather than refused, which means your key has already crossed the network in the clear before the redirect is followed."
              }
            },
            {
              "name": "Too many full catalogues — 429 with Retry-After",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "// Run '1. Push the full catalogue' immediately before this one to see it.",
                      "if (pm.response.code === 429) {",
                      "  pm.test('tells you how long to wait, in seconds', () => {",
                      "    pm.expect(pm.response.headers.get('Retry-After')).to.be.a('string');",
                      "  });",
                      "  pm.test('and says so in words', () => {",
                      "    pm.expect(pm.response.json().detail).to.include('5 minutes');",
                      "  });",
                      "} else {",
                      "  pm.test.skip('not rate limited right now — push a catalogue first, then re-run');",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "PUT",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/api/partners/v1/catalog",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "api",
                    "partners",
                    "v1",
                    "catalog"
                  ]
                },
                "description": "One full catalogue every 5 minutes, 60 updates a minute, 120 status checks a minute — per merchant, counted across all our servers.\n\nHonour `Retry-After` rather than retrying on a loop.",
                "body": {
                  "mode": "raw",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  },
                  "raw": "{\n  \"items\": [\n    {\n      \"external_id\": \"SKU-1021\",\n      \"name\": \"iPhone 16 Pro 256GB\",\n      \"price\": 389.5,\n      \"currency\": \"KWD\",\n      \"url\": \"https://example-shop.com/p/iphone-16-pro\",\n      \"images\": [\"https://picsum.photos/seed/shamz-ip16pro/800/800\"]\n    }\n  ]\n}"
                }
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Widget Chat",
      "description": "Send a message to a deployed assistant and get its reply.\n\nAuthenticated with the skill's widget API key in `X-API-KEY` — a different credential from the Push API key above.\n\nMost people never call this directly: the embeddable widget does it for you (see the Widget Embed section of the developer guide). Call it yourself when you are building your own front end.",
      "auth": {
        "type": "apikey",
        "apikey": [
          {
            "key": "key",
            "value": "X-API-KEY",
            "type": "string"
          },
          {
            "key": "value",
            "value": "{{widgetApiKey}}",
            "type": "string"
          },
          {
            "key": "in",
            "value": "header",
            "type": "string"
          }
        ]
      },
      "item": [
        {
          "name": "Send a message",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));",
                  "",
                  "const b = pm.response.json();",
                  "",
                  "pm.test('answers with a reply', () => {",
                  "  pm.expect(b).to.have.property('reply');",
                  "});",
                  "",
                  "// Keep the session_id to continue the same conversation. Without it",
                  "// every message starts a new one and the assistant has no history.",
                  "if (b.session_id) {",
                  "  pm.collectionVariables.set('sessionId', b.session_id);",
                  "  console.log('continue this conversation with session_id', b.session_id);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/widget/chat",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "widget",
                "chat"
              ]
            },
            "description": "`msg` carries the message as `{type, content}` — `type` is `text` for an ordinary message.\n\n`session_id` is what ties turns together. Leave it out on the first message and keep the one you get back; send it on every message after that, or the assistant starts over each time.\n\nThe reply is not always plain text. Depending on the skill, the assistant may answer with a calendar of bookable slots, a confirmation to accept, an intake form, or a created ticket — so read the response shape rather than assuming `reply` is the whole answer.\n\n`language` accepts a two-letter code and is worth sending when you already know it; otherwise it is detected from the message.",
            "body": {
              "mode": "raw",
              "options": {
                "raw": {
                  "language": "json"
                }
              },
              "raw": "{\n  \"msg\": {\n    \"type\": \"text\",\n    \"content\": \"Hello — what are your opening hours?\"\n  },\n  \"session_id\": \"{{sessionId}}\",\n  \"language\": \"en\"\n}"
            }
          },
          "response": [
            {
              "name": "200 — a text answer",
              "status": "OK",
              "code": 200,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"reply\": \"We are open Sunday to Thursday, 9am to 6pm.\",\n  \"suggestions\": [\"Book an appointment\", \"Where are you located?\"]\n}"
            }
          ]
        }
      ]
    }
  ]
}