Skip to main content
Auth via the Client SDK key (osk_ prefix). Async kickoff + polling — POST starts a round and returns identifiers immediately; client polls the detail endpoint for status.

Model

  • chat_id = one conversation. Issued on the first round; reuse it for follow-ups.
  • round_id = one round. Each POST = one round, holding its own prompt + result snapshot.
  • Follow-up context (since 2026-05-19): every prior COMPLETED round on the same chat is automatically replayed to llm-demo as a (user prompt, assistant meal JSON) pair via conversation_history. Clients just need to send {chat_id, prompt} — short refinement utterances (“add wine”, “no pork this time”) are correctly anchored against the prior round’s ingredients. Rounds whose meal cannot be reconstructed are silently skipped.
  • Response language: resolved from profile.locale first, then the tenant’s default language, then ko. Message-style text (recipe_title / intro_message / recipe_description / recommendation_message / notes …) is emitted in that language. Supported: ko / en / ja.
  • reason field is an exception: it surfaces in the tenant admin UI and must switch display language without a refetch when the operator toggles the UI locale, so it always arrives as a {ko, en, ja} object with all three locales populated.
  • Follow-up cap: more than 8 refinements within the same round → 409 REFINEMENT_CAP_EXCEEDED. Reset by starting a new round (POST) or a new chat.
  • In-flight round guard (since 2026-05-20): a new POST returns 409 PRIOR_ROUND_IN_FLIGHT only while the prior round’s step2 hasn’t completed yet. Once pipeline_progress.step2 == "done" (todos populated), follow-ups are accepted immediately even with step3/step4 still running in the background. Rounds are serialized up to the step2 boundary; step3/4 run independently per round.

1. Kickoff a recommendation

POST /v1/chat/{profile_id}/recommend
New chat or follow-up on an existing chat. Returns 200 with identifiers immediately; poll the detail endpoint for the result.

Request

{
  "chat_id": "(Optional) UUID",
  "prompt":  "What should I cook for dinner?"
}
FieldTypeRequiredDescription
chat_idstringnoOmit to start a new chat; pass an existing one to append a follow-up round
promptstringyesUser’s natural-language query
🧑 persona is auto-injected from the profile — no separate body field. The server reads data.persona from GET /v1/profile/{profile_id} and, if non-empty, prepends persona(페르소나: ...) into the user_query prelude. Update it at profile time via PUT /v1/profile/{profile_id} with a persona field (free-form string, nullable).
fire-and-forget — this endpoint returns immediately (~100ms). The LLM call runs in the background; poll for results. LLM errors like RefinementCapExceeded are surfaced as round.status=error + last_error (not as synchronous 4xx).

Response 202

{
  "success": true,
  "data": {
    "chat_id":  "a14b2f9c-3d5e-4f2a-9a8b-1c2d3e4f5a6b",
    "round_id": "8f53c2b0-..."
  }
}
Then poll GET /v1/chat/{profile_id}/recommend/{chat_id} until the last round’s pipeline_progress.step4 == "done".

2. List chats (paged)

GET /v1/chat/{profile_id}/recommend
QueryDefaultDescription
page00-indexed
limit100page size (max 500)
orderdescsort by created_at, desc / asc

Response 200

{
  "success": true,
  "data": {
    "items": [
      {
        "chat_id":      "...",
        "total_rounds": 2,
        "rounds": [
          { "round_no": 1, "round_id": "..." },
          { "round_no": 2, "round_id": "..." }
        ],
        "created_at":   "2026-04-29T05:28:08Z"
      },
      {
        "chat_id":      "...",
        "total_rounds": 1,
        "rounds": [
          { "round_no": 1, "round_id": "..." }
        ],
        "created_at":   "2026-04-28T22:11:00Z"
      }
    ],
    "pageInfo": {
      "total": 42,
      "page":  0,
      "limit": 100,
      "order": "desc"
    }
  }
}
Each item carries chat_id + total_rounds + a mini rounds[] of {round_no, round_id} only + created_at. Round bodies (prompt / results) come from the detail or single-round endpoints. Pagination metadata is wrapped in pageInfo.

3. Chat detail (rounds[])

GET /v1/chat/{profile_id}/recommend/{chat_id}
This is the polling target. Each call refreshes any in-flight (KICKED_OFF) rounds against llm-demo and persists the snapshot once the pipeline lands. Already-completed rounds are returned as-is.

Response 200 — in flight (last round still pending)

🔑 Schema consistency (since 2026-05-09) — the snapshot keys are always present even when pipeline_result is still empty. Clients should branch on values (pipeline_progress.stepN == "done", items.length > 0) rather than on key presence.
{
  "success": true,
  "data": {
    "chat_id":         "a14b2f9c-...",
    "profile_id":      "6b97957c-...",
    "profile_summary": { "name": "Sato Hanako", "gender": "F", "age": 32, "locale": "ja" },
    "created_at":      "2026-04-29T05:28:08Z",
    "rounds": [
      {
        "round_no":         1,
        "round_id":         "8f53c2b0-...",
        "prompt":           "What should I cook for dinner?",
        "created_at":       "2026-04-29T05:28:08Z",
        "updated_at":       "2026-04-29T05:28:08Z",
        "status":           "in_progress",
        "back_list_status": null,
        "pipeline_progress": {
          "step1": "running",
          "step2": "pending",
          "step3": "pending",
          "step4": "pending"
        },
        "todos":             null,
        "recommended_items": [],
        "notifications":     { "ad": null, "coupon": [] }
      }
    ]
  }
}
When each step’s data becomes available (from the moment pipeline_progress.stepN == "done"):
  • step1 (~5s): no dedicated keys filled (LLM response complete + meal plan generated)
  • step2 (~10s): todos.recipe_title / intro_message / recipe_description / recommendation_message / ingredients[] / items[].product populated
  • step3 (~15s): recommended_items[] populated (reason = category tag, reason_detail = analytical text, reason_detail_marketing = customer copy)
  • step4 (~20s): notifications.ad.items (signage) + notifications.coupon[] (in-app coupons) populated
  • notifications.coupon_spot[] is step-independent — the store’s coupon-spot master is fetched from store-API on every poll (not an LLM recommendation)
{
  "success": true,
  "data": {
    "chat_id":      "...",
    "profile_id":   "...",
    "profile_summary": { ... },
    "created_at":   "...",
    "rounds": [
      {
        "round_no":         1,
        "round_id":         "...",
        "prompt":           "What should I cook for dinner?",
        "created_at":       "...",
        "updated_at":       "...",
        "status":           "completed",
        "back_list_status": "completed",
        "todos": {
          "recipe_title":           "Spicy Beef Curry",
          "intro_message":          "How about spicy beef curry tonight? I've put together your shopping list.",
          "recipe_description":     "Japanese-style curry...",
          "recommendation_message": "A great pick for a day like today — personalized rationale derived from the profile signals.",
          "ingredients": [
            {
              "name":          { "ko": "두부",  "en": "tofu",         "jp": "豆腐" },
              "category":      "Pantry",
              "quantity":      1,
              "quantity_unit": { "ko": "모",    "en": "block",        "jp": "丁" }
            },
            {
              "name":          { "ko": "배추",  "en": "napa cabbage", "jp": "白菜" },
              "category":      "Produce",
              "quantity":      0.25,
              "quantity_unit": { "ko": "포기",  "en": "head",         "jp": "玉" }
            }
          ],
          "items": [
            {
              "quantity": 1,
              "product":  { "id": 196, "name": "...", "price": 5900, "price_unit": "₩", ... }
            }
          ],
          "total": 5900
        },
        "recommended_items": [
          {
            "rec_id":                   "uuid-...",
            "reason":                   "cross-sell",
            "reason_detail":            { "ko": "Cross-sell ...", "en": "Cross-sell ...", "ja": "Cross-sell ..." },
            "reason_detail_marketing":  { "ko": "Pairs naturally for a fuller table.", "en": "Pairs naturally for a fuller table.", "ja": "Pairs naturally for a fuller table." },
            "product":                  { "id": 512, "name": "Cabernet Sauvignon 750ml", ... }
          }
        ],
        "notifications": {
          "ad": {
            "signage": { "x": 4, "y": 1, "radius": 2 },
            "items": [
              {
                "item_id":                196,
                "rec_id":                 "uuid-...",
                "item_name":              "Higeta Soy Sauce 1L",
                "item_name_en":           "Higeta Soy Sauce 1L",
                "brand":                  "Higeta",
                "zone_id":                12,
                "zone_name":              "Pantry",
                "type":                   "ads",
                "discount_pct":           5,
                "applied_score":          0.85,
                "priority_score":         0.95,
                "method":                 "copurchase_lift",
                "reason_type":            "cross-sell",
                "reason_detail":          { "ko": "Cross-sell ...", "en": "Cross-sell ...", "ja": "Cross-sell ..." },
                "reason_detail_marketing":{ "ko": "Pairs well with curry.", "en": "Pairs well with curry.", "ja": "Pairs well with curry." },
                "rank":                   1,
                "price":                  860,
                "price_unit":             "¥",
                "product":                { "id": 196, "name": "...", "price": 860, "price_unit": "¥", "location": { "x": 4, "y": 1, "radius": 2 }, "...": "..." }
              }
            ]
          },
          "coupon": [
            {
              "item_id":      881,
              "rec_id":       "uuid-...",
              "item_name":    "...",
              "type":         "coupon",
              "zone_id":      76,
              "discount_pct": 10,
              "price":        3600,
              "price_unit":   "¥",
              "...":          "...",
              "product": {
                "id":           881,
                "name":         "Roasted Sesame Dressing 380ml",
                "brand":        "Kewpie",
                "barcode":      "4906028184943",
                "price":        3600,
                "price_unit":   "¥",
                "src_url":      "https://cdn.ones1ght.com/store/product/no_image.png",
                "quantity":     100,
                "section_code": "PA",
                "section_name": "Pantry",
                "location":     { "x": 28, "y": 22.794, "radius": 0.5 }
              },
              "location":     { "x": 28, "y": 22.794, "radius": 0.5 }
            }
          ],
          "coupon_spot": [
            {
              "item_id":                42,
              "rec_id":                 null,
              "item_name":              "Protein Bar 50g",
              "item_name_en":           "Protein Bar 50g",
              "brand":                  "...",
              "zone_id":                null,
              "zone_name":              null,
              "type":                   "coupon",
              "discount_pct":           10,
              "applied_score":          null,
              "priority_score":         null,
              "method":                 null,
              "reason_type":            null,
              "reason_detail":          null,
              "reason_detail_marketing":null,
              "rank":                   1,
              "price":                  1200,
              "price_unit":             "¥",
              "product":                { "id": 42, "name": "...", "price": 1200, "...": "..." },
              "location":               { "x": 12.3, "y": 4.5, "radius": 1.0 }
            }
          ]
        }
      },
      {
        "round_no": 2,
        "round_id": "...",
        "prompt":   "Something other than wine?",
        "...": "follow-up — previous prompt is sent as context"
      }
    ]
  }
}
Round fieldDescription
round_no1-indexed position — which question this round answered. The last item’s round_no equals the total round count.
statusin_progress / completed / error
back_list_statusllm-demo’s not_started / in_progress / completed (null until first snapshot)
todosShopping list with recipe metadata. Each item is {quantity, product}; total = Σ(product.price × quantity)
todos.recommendation_messagestep1 (meal) personalized rationale that reflects profile signals (name / gender / age / extra) — a human-friendly comment, distinct from recipe_description (generic dish blurb). Emitted in the profile locale
todos.itemsOrdered by in-store pickup route (since 2026-05-20) — stable sort on product.section_code. Order: produce(PR) → deli(DL) → bakery/sweets(BK/SW/SN) → alcohol(AL) → dairy/drinks(DY/DR) → pantry(PA) → meat(MT) → seafood(FI) → frozen(FR). So a picker can walk the categories in a single sweep. Codes outside the table (EC/HH/CK, …) go to the tail. Original order is preserved within a category
recommended_itemsMeal-matching cross-sells. reason = category tag ("cross-sell" / "upsell" / "impulse"), reason_detail = admin/dashboard analytical text (lift / CVR / co_count), reason_detail_marketing = customer-facing copy, product = store-merged catalog row. reason_detail / reason_detail_marketing are always {ko, en, ja} objects — while upstream emits a single locale, the same value is mirrored across all three keys (shape-only consistency)
notifications.adSignage ads bundle { signage, items }. signage = single signage’s {x, y, radius} (or null). items = type=ads entries (top 3). 1-indexed rank by priority_score desc
notifications.couponIn-app coupon entries. Each carries location {x, y, radius} resolved from the item’s item_id → store product → Section coords (null when no match)
notifications.coupon_spotStore-side coupon-spot master (not an LLM recommendation; fetched from store-API on every poll). Same 19-field schema as coupon[] so iOS can decode both with the same logic. type = "coupon"; recommendation signals (rec_id, reason_*, applied_score, priority_score, method) are all null. discount_pct ← admin discount_value; rankspot_no ASC index. location is the spot’s own {x, y, radius} rather than product.location
notifications.*[].item_idStore catalog product id. May be null when llm-demo can’t attach it — entry still ships with item_name / item_name_en
notifications.*[].productFull store catalog row — id / name / brand / barcode / price / price_unit / src_url (image) / location / section_code / section_name / quantity (null when no match)
notifications.*[].price / price_unitConvenience flattening of product.price / product.price_unit for card price hot-path (both null when no match). The legacy price_yen field is dropped — store DB is the single source of truth
notifications.*[].type"ads" (signage) / "coupon" (in-app). Same as the group key but kept on each entry too
notifications.*[].rec_idRecommendation event id — pass back as items[].rec_id on /trigger/signage or rec_id on /trigger/coupon
notifications.*[].reason_type / reason_detail / reason_detail_marketingStep4 reason triple — reason_type = category tag ("cross-sell" / "zone_efficiency" etc.), reason_detail = admin/dashboard analytical text, reason_detail_marketing = customer copy rendered on signage / coupon. reason_detail / reason_detail_marketing are {ko, en, ja} objects (mirrored across keys while upstream emits a single locale)
last_errorSet on status: error only (e.g. meal_suggest failed: upstream timeout / upstream chat session expired)

4. Single round

GET /v1/chat/{profile_id}/recommend/{chat_id}/{round_id}
Returns just one round. Same auto-refresh contract as the detail endpoint — KICKED_OFF / CREATED rounds get a fresh upstream check before the response is built — so you can poll a single round directly when that’s all you need. Response shape mirrors detail.rounds[i] (no chat metadata / profile_summary wrapper):
{
  "success": true,
  "data": {
    "round_no":         2,
    "round_id":         "...",
    "prompt":           "...",
    "created_at":       "...",
    "updated_at":       "...",
    "status":           "completed",
    "back_list_status": "completed",
    "todos":            { ... },
    "recommended_items":  [ ... ],
    "notifications":    { "ad": { "signage": ..., "items": [...] }, "coupon": [...] }
  }
}
Ownership chain (tenant + api_key + profile + chat → round) must line up; mismatch → NOT_FOUND 404.

5. Send signage ads trigger — POST /trigger/signage

Called by the iOS sdk on UWB enter / exit of a signage location-radius. The action field branches behavior (defaults to "enter").
  • enter: forwards the round’s notifications.ad.items to the signage → RECOGNIZED screen. Server (1) persists one sdk_chat_event per item (2) fans out to tenant admin SSE (3) forwards to store-API.
  • exit: signage returns to IDLE immediately. No persistence / SSE, store-API forward only.

Per-signage rendering policy

The gateway forwards the full items[] to store-API; which item to show and how is decided by the signage client (per signage_no).
SignageIDLE screenACTIVE (RECOGNIZED) data source
#1 (entrance signage)1~4 admin-curated slides (cross-fade)Sorts items by discount_pct desc and shows the top 1 as a single product card. Auto-orients landscape/portrait. reason_detail becomes the marketing copy
#2 (in-store signage)9-slot admin grid (3 slides × 3 products)Three sections — ① forgotten[0] (missed item) ② items[1] || items[0] (ad, index-based) ③ one random enabled=true row from store-API signage_event
Empty sections are auto-hidden (e.g. no forgotten → that area is omitted). Signage #1 is a single-card design so discount_pct ordering matters; #2 displays the ad next to “missed / event” so it uses index 1 first (index 0 is reserved for the stronger signal elsewhere).
POST /v1/chat/{profile_id}/trigger/signage
Authorization: Bearer {SDK_KEY}
Content-Type: application/json

Request — Enter (area entry)

The minimal payload is just chat_id + round_id. With action omitted, "enter" is the default. Gateway resolves the round’s persisted notifications.ad.items and fires ADS_TRIGGERED for each.
{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe"
}

Request — Exit (area exit)

{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe",
  "action":   "exit"
}
Response: { "success": true, "data": { "event_ids": [], "accepted": true, "action": "exit" } }

Request — Enter (items override + forgotten)

You can also pass items explicitly to override (live data, partial set, etc.). Use forgotten to attach items the user left behind in-store (since 2026-05-20):
{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe",
  "items": [
    {
      "rec_id":        "rec_a1b2c3",
      "item_id":       196,
      "item_name":     "Spaghettini",
      "item_name_en":  "Spaghettini",
      "brand":         "De Cecco",
      "zone_id":       12,
      "zone_name":     "Pantry",
      "price_yen":     4900,
      "discount_pct":  17,
      "applied_score": 0.85,
      "priority_score":0.95,
      "method":        "copurchase_lift",
      "reason_detail": "...",
      "reason_detail_marketing": "..."
    }
  ],
  "forgotten": [
    { "item_id": 882, "name": "Onion", "quantity": 1 },
    { "any": "shape", "is": "fine" }
  ]
}
FieldRequiredDescription
chat_idThe chat the recommendation came from
round_idThe round
itemsoptionalOmit / empty → Gateway looks up the round’s persisted ad items. Provided values take precedence
items[].rec_id✅ (when items sent)Per-item recommendation event id
items[].item_id / item_name / item_name_en / brand / zone_id / zone_name / price_yen / discount_pct / applied_score / priority_score / method / reason_type / reason_detail / reason_detail_marketingoptionalEchoed to FIREHOSE + signage. Rendering policy depends on signage_no — see the “Per-signage rendering policy” table above (#1 = discount_pct top 1 single card, #2 = items[1] || items[0] ad + forgotten + event in 3 sections)
forgottenoptionalItems the user left behind in-store. iOS sdk attaches the relevant todos.items entries verbatim. Each element’s schema is free-form (any) — Gateway passes the array through to the persisted payload, the admin SSE fanout, and the store-API forward at the root level without any transformation. Empty / omitted → key absent. Ignored on exit

Response 202

{ "success": true, "data": { "event_ids": ["evt_..."], "accepted": true } }
event_ids length matches persisted items. Frequency = location entries (one round + one signage = 1 call). For each item, the server lifts step4 store_reasoning.analyst_comment from the raw pipeline and attaches it as analyst_comment on the SSE payload — surfaced as the rationale on the admin user-analytics signage event.

6. Send coupon issued/redeemed — POST /trigger/coupon

Called by the iOS sdk per notifications.coupon[] entry when issued / redeemed / dismissed inside the app. Signage-agnostic — only (1) persistence and (2) tenant admin SSE fanout.
POST /v1/chat/{profile_id}/trigger/coupon
Authorization: Bearer {SDK_KEY}
Content-Type: application/json

Request

The minimal payload is chat_id + round_id + rec_id. Gateway matches rec_id against the round’s persisted notifications.coupon[] and fills the rest of the metadata automatically.
{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe",
  "rec_id":   "rec_x9y8z7"
}
Sending the full metadata still works — provided values take precedence:
{
  "chat_id":       "c_91ab",
  "round_id":      "r_77fe",
  "rec_id":        "rec_x9y8z7",
  "kind":          "issued",
  "item_id":       512,
  "item_name":     "Olive Oil",
  "item_name_en":  "Olive Oil",
  "brand":         "Colavita",
  "zone_id":       76,
  "zone_name":     "Pantry",
  "price_yen":     9900,
  "discount_pct":  20,
  "applied_score": 0.78,
  "priority_score":0.72,
  "method":        "copurchase_lift",
  "reason_detail": "...",
  "reason_detail_marketing": "..."
}
FieldRequiredDescription
chat_id / round_idSame as above
rec_idRecommendation event id (match key against the round snapshot)
kindoptional"issued" / "redeemed" / "dismissed" (default issued)
OthersoptionalAuto-filled from the matched round entry when omitted; provided values take precedence

Response 202

{ "success": true, "data": { "event_id": "evt_...", "accepted": true } }

7. Personal recommendation impression log — POST /trigger/recommend

Called by the iOS sdk as a one-shot log when an item from recommended_items[] (step3) is shown or tapped inside the app. Signage / store-API agnostic — only (1) sdk_chat_event persistence (2) tenant admin SSE fanout.
POST /v1/chat/{profile_id}/trigger/recommend
Authorization: Bearer {SDK_KEY}
Content-Type: application/json

Request

{
  "chat_id":  "c_91ab",
  "round_id": "r_77fe",
  "rec_id":   "rec_a1b2c3"
}
FieldRequiredDescription
chat_id / round_idSame as above
rec_idRecommendation event id (recommended_items[].rec_id or id)
Gateway matches rec_id against the round’s recommended_items[] and auto-fills item_name / price / reason_detail / reason_detail_marketing into the payload. The step3 surface_reasoning.analyst_comment is also extracted from the raw pipeline and attached as analyst_comment — surfaced as the rationale on the admin user-analytics personal-recommendation event.

Response 202

{ "success": true, "data": { "event_id": "evt_...", "accepted": true } }

8. Round barcode bundle — GET /recommend/{chat_id}/{round_id}/barcodes

Returns the round’s products grouped by category alongside EAN-13 PNG (base64). Admin / demo-only — no store-API re-fetch (uses the round’s flattened data + on-the-fly ZXing PNG).
GET /v1/chat/{profile_id}/recommend/{chat_id}/{round_id}/barcodes
Authorization: Bearer {SDK_KEY}

Response 200

{
  "success": true,
  "data": {
    "chat_id":  "...",
    "round_id": "...",
    "todos":         [ /* shopping-list products */ ],
    "recommends":    [ /* personal recommendation products */ ],
    "notifications": {
      "ads":    [ /* signage ad products */ ],
      "coupon": [ /* coupon products */ ]
    }
  }
}
Each product entry — 5 fields only:
{
  "name":          "Tofu",
  "barcode":       "8801234567890",
  "price":         1980,
  "price_unit":    "₩",
  "barcode_image": "data:image/png;base64,iVBORw0KGgo..."
}
  • When barcode isn’t 13 digits or fails the checksum, barcode_image is null — clients only need a null guard.
  • The same product can appear in multiple categories.

Polling guidelines

  • Recommended interval: ~2 s (full pipeline averages ~46 s)
  • Inspect only the last rounds[] entry — earlier rounds are always completed (or error)
  • After a follow-up POST, the array length grows by 1; the new last item starts at in_progress
  • On error / upstream TTL expiry: start a fresh chat (POST without chat_id)

Error codes

HTTPcodeCause
400INVALID_REQUESTBody validation failed (missing prompt, etc.)
404NOT_FOUNDprofile / chat missing or ownership mismatch
402INSUFFICIENT_CREDITTenant credit balance too low
409REFINEMENT_CAP_EXCEEDEDMore than 8 follow-ups within one round — reset by starting a new round / new chat
409PRIOR_ROUND_IN_FLIGHTPrior round’s step2 hasn’t completed yet — wait for pipeline_progress.step2 == "done". step3/4 still running is fine
500INTERNAL_ERRORUpstream failure

Full walkthrough

SDK_KEY="osk_..."
PROFILE_ID="..."

# 1. First round — no chat_id
RESP=$(curl -s -X POST https://api.ones1ght.com/v1/chat/$PROFILE_ID/recommend \
  -H "Authorization: Bearer $SDK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Dinner ideas?"}')
CHAT_ID=$(echo "$RESP" | jq -r '.data.chat_id')

# 2. Poll detail (2 s) until the last round flips to completed
while true; do
  DETAIL=$(curl -s https://api.ones1ght.com/v1/chat/$PROFILE_ID/recommend/$CHAT_ID \
    -H "Authorization: Bearer $SDK_KEY")
  LAST_STATUS=$(echo "$DETAIL" | jq -r '.data.rounds[-1].status')
  [ "$LAST_STATUS" = "completed" ] && { echo "$DETAIL" | jq '.data.rounds[-1]'; break; }
  [ "$LAST_STATUS" = "error" ]     && { echo "$DETAIL" | jq '.data.rounds[-1].last_error'; break; }
  sleep 2
done

# 3. Follow-up on the same chat_id
curl -X POST https://api.ones1ght.com/v1/chat/$PROFILE_ID/recommend \
  -H "Authorization: Bearer $SDK_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"chat_id\":\"$CHAT_ID\",\"prompt\":\"Something other than wine?\"}"

# 4. Cumulative list
curl https://api.ones1ght.com/v1/chat/$PROFILE_ID/recommend?page=0\&limit=20 \
  -H "Authorization: Bearer $SDK_KEY"