Send a message

post/v1/chat

Retired. This endpoint answers 410 Gone and no longer works. It is documented for reference only; use the v2 agent API's Chat instead.

Send a shopper message to the stylist and receive a structured plan: a natural-language reply, a list of slots (one per garment the stylist wants to find, each with a semantic query and filters), and a status telling you whether the request was resolved or the stylist needs_clarification.

The stylist only plans the outfit. Slots come back with an empty results array; call the Search endpoint with the chatId and the slot's id to fill each slot with products from your catalogue.

Starting vs. continuing a conversation

  • Omit chatId to start a new conversation. Every message in messages is stored in the new chat (so you can seed it with a prior exchange), and the new id is returned as chatId.
  • Pass chatId to continue an existing conversation. The stored history is loaded from the server and only the last entry of messages is appended; any earlier entries in the request are ignored. Public keys can continue any chat that belongs to the same organization.

Message normalization

Messages are normalized before validation, so several shapes are accepted:

  • A user message whose content is a plain string becomes a single input_text part.
  • An assistant message whose content is a plain string is parsed as JSON ({ "message", "slots", "status" } or { "output": {...} }); if it is not JSON it is treated as the reply text. Assistant messages are always rewritten into the three-part output_text / output_slots / output_status form.
  • An output_status value that is not resolved or needs_clarification (for example a stale streaming marker replayed from a client) is coerced to resolved rather than rejected, and any generating flag is dropped.
  • system messages are forwarded to the model unchanged.

Anything that still fails validation after normalization is rejected with 422 and a message naming the offending index.

Images

A user message may include input_image parts. image_url can be a public https:// URL, a data: URI, or a Stylor image-delivery URL (/api/v1/image/...) from a previous upload — the latter is converted to a short-lived signed CDN URL before it reaches the model.

Catalogue scope

datasetIds restricts which datasets the stylist can plan against. When omitted or empty, every active dataset in the organization is used. Archived datasets are skipped (with a warning); draft datasets are used but flagged in warnings. filters.gender narrows the catalogue to that gender's items plus unisex items.

Streaming

Set stream: true to receive the same result as a Server-Sent Events stream: the reply text arrives as it is written and each slot is delivered the moment it is complete, so you can start searching before the stylist has finished talking. See the Streaming guide and the event list below.

The non-streaming response is produced by consuming that same stream server-side and returning its final payload as one JSON body.

Public keyapi.v1.chat.messageDemo key OKStreams SSEDeprecated

Request body

application/jsonrequired
array<ChatMessage>required

The conversation. With no chatId every entry is stored and used; with a chatId only the last entry is appended to the stored history.

minItems: 1
rolestringrequired

Who wrote the message. system messages are passed to the model verbatim.

One ofuserassistantsystem
e.g. "user"
string | array<ChatMessageContentPart>required

Plain text, or an array of content parts.

chatIdstring (uuid)

Id of an existing chat to continue. Omit to start a new chat.

e.g. "3f0c2a4e-8b1d-4c6a-9e2f-7a5b1c0d9e88"
datasetIdsarray<string (uuid)>

Dataset ids to plan against. Omit or pass [] to use every active dataset. Ids are trimmed and de-duplicated; archived ids are dropped with a warning and unknown ids are rejected.

default: []
ChatFilters

Catalogue-level filters for the whole request. Keys other than gender are ignored.

genderstring

Restrict the catalogue to this gender's items plus unisex items. Omit (or pass unisex) to use unisex items only. Any other string is not rejected but matches only unisex items.

One ofmenswomensunisex
e.g. "womens"
streamboolean

When true the response is a text/event-stream (see the stream events below) instead of JSON.

default: false

Response

200The stylist's plan. Returned as JSON when stream is omitted or false, or as text/event-stream when stream is true. Both carry the rate-limit headers.
application/json
ChatOutputrequired

The stylist's plan for one turn.

statusstringrequired

resolved when an outfit was planned; needs_clarification when the stylist asked a follow-up question instead (and slots is empty).

One ofresolvedneeds_clarification
e.g. "resolved"
messagestringrequired

The stylist's reply to show the shopper.

e.g. "For a rooftop dinner I'd pair a silk slip dress with a cropped leather jacket and strappy heels."
array<ChatSearchSlot | ChatOutfitCompositionsSlot>required

The items to find, in outfit order, plus an optional outfit_compositions slot.

chatIdstring (uuid)required

The chat the turn was written to. Equal to the request's chatId, or the newly created id.

e.g. "3f0c2a4e-8b1d-4c6a-9e2f-7a5b1c0d9e88"
array<string | object>

Non-fatal notices. Present only when there is at least one.

text/event-stream
string

A stream of event: <name>\ndata: <json>\n\n frames. The HTTP status is always 200 once the stream has started; a failure during generation is delivered as an error event and the stream is closed. Errors detected before the stream starts (validation, unknown chat, empty catalogue, quota) are returned as an ordinary JSON error response with the same status codes listed under Errors.

e.g. "event: chat_id\ndata: {\"chatId\":\"3f0c2a4e-8b1d-4c6a-9e2f-7a5b1c0d9e88\"}\n\nevent: text_delta\ndata: {\"delta\":\"For a rooftop dinner \"}\n\nevent: slot_complete\ndata: {\"slot\":{\"id\":\"8066a68a-a39e-4d3e-993a-3887f6245032\",\"type\":\"search\",\"slot\":\"dress\",\"query\":\"black silk slip dress\",\"filters\":{\"fit\":[\"slim\"],\"color_family\":[\"black\"],\"color_rgb\":[20,20,20],\"family\":[\"dress\"],\"exclude_terms\":[],\"boost_terms\":[\"silk\"],\"minPrice\":null,\"maxPrice\":180},\"results\":[]}}\n\nevent: done\ndata: {\"output\":{\"status\":\"resolved\",\"message\":\"For a rooftop dinner I'd pair a silk slip dress with a cropped leather jacket.\",\"slots\":[...]},\"chatId\":\"3f0c2a4e-8b1d-4c6a-9e2f-7a5b1c0d9e88\"}\n"

Stream events

5

Each frame is event: <name> followed by data: <json> and a blank line. Events are listed in the order they normally arrive.

chat_id

Always the first frame. Carries the id of the chat the reply is being written to (the new id when chatId was omitted), so you can issue Search calls against it before generation finishes.

data
{
  "chatId": "3f0c2a4e-8b1d-4c6a-9e2f-7a5b1c0d9e88"
}
text_delta

A new chunk of the stylist's reply text. Concatenate every delta, in order, to rebuild output.message. Zero or more of these arrive, interleaved with slot_complete.

data
{
  "delta": "For a rooftop dinner I'd pair "
}
slot_complete

A slot the stylist has finished describing. search slots are emitted once slot, query and the core filter keys (fit, color_family, color_rgb, family, exclude_terms, boost_terms, minPrice, maxPrice) are present; pattern and material may still be missing at this point and are filled in on the copy inside done. outfit_compositions slots are emitted as soon as their type appears. The slot is persisted to the chat before this frame is sent, so its id is immediately valid for the Search endpoint. results is always [] here.

data
{
  "slot": {
    "id": "8066a68a-a39e-4d3e-993a-3887f6245032",
    "type": "search",
    "slot": "dress",
    "query": "black silk slip dress",
    "filters": {
      "fit": [
        "slim"
      ],
      "color_family": [
        "black"
      ],
      "color_rgb": [
        20,
        20,
        20
      ],
      "family": [
        "dress"
      ],
      "exclude_terms": [
        "maxi"
      ],
      "boost_terms": [
        "silk"
      ],
      "minPrice": null,
      "maxPrice": 180
    },
    "results": []
  }
}
done

Final frame on success; the stream closes after it. output is the complete plan — the same shape as the JSON response — with every slot fully merged (same ids as the slot_complete frames, plus any slot that only appeared in the final parse). warnings is present only when non-empty.

data
{
  "output": {
    "status": "resolved",
    "message": "For a rooftop dinner I'd pair a silk slip dress with a cropped leather jacket and strappy heels.",
    "slots": [
      {
        "id": "8066a68a-a39e-4d3e-993a-3887f6245032",
        "type": "search",
        "slot": "dress",
        "query": "black silk slip dress",
        "filters": {
          "fit": [
            "slim"
          ],
          "pattern": [
            "solid"
          ],
          "material": [
            "silk"
          ],
          "color_family": [
            "black"
          ],
          "color_rgb": [
            20,
            20,
            20
          ],
          "family": [
            "dress"
          ],
          "exclude_terms": [
            "maxi"
          ],
          "boost_terms": [
            "silk"
          ],
          "minPrice": null,
          "maxPrice": 180
        },
        "results": []
      },
      {
        "id": "5b2e9f40-3c7d-4e1a-9f6b-8d0c2a4e6f17",
        "type": "outfit_compositions",
        "results": []
      }
    ]
  },
  "chatId": "3f0c2a4e-8b1d-4c6a-9e2f-7a5b1c0d9e88",
  "warnings": [
    "Failed to save chat messages."
  ]
}
error

Final frame on failure, sent instead of done; the stream closes after it. The HTTP status is still 200 because headers were already sent. error is either Invalid AI response format. (the model's output could not be parsed) or Looks like I ran into some trouble generating your outfit. Let's try that again. (the model call failed). The chat's placeholder assistant message is finalized with that text so the conversation can continue.

data
{
  "error": "Looks like I ran into some trouble generating your outfit. Let's try that again."
}

Errors

51

Every error is JSON with a single error string unless noted. Match on the status code; the message is for humans.

400
Request body must be valid JSON.

The request body could not be parsed as JSON.

400
Request body must be a JSON object.

The request body is empty, or parsed to something other than a JSON object (for example an array or a string).

404
Chat not found or you do not have permission to access it.

chatId was provided but no chat with that id belongs to the organization.

404
No active datasets found for this organization. Activate one via the dashboard (Dataset → Settings) or through the REST API before proceeding.

datasetIds was omitted or empty and the organization has datasets, but none is active.

404
All requested datasets are archived. Reactivate at least one via the dashboard or REST API.

Every id in datasetIds refers to an archived dataset.

404
No datasets found for this organization. Create one via the dashboard or REST API, or pass datasetIds to use draft/archived datasets before proceeding.

The organization has no datasets at all.

404
No products found in selected datasets.

The resolved datasets contain no fashion items for the requested gender (non-fashion categories such as phone cases are ignored).

410
This endpoint has been retired (v1/chat).

Always, for API callers. This check runs before authentication and consumes no quota.

422
The 'messages' field must be an array.

messages is present but not an array (including null).

422
A valid chatId is required.

chatId was provided but is not a string (for example a number) or is only whitespace.

422
Message at index <n> must be an object.

An entry of messages is not an object.

422
Message at index <n> must have a 'role' field.

A message has no string role.

422
Message at index <n> has invalid role '<role>'. Must be one of: user, assistant, system.

role is anything other than user, assistant or system (developer is not accepted).

422
Message at index <n> must have a 'content' field.

content is missing or null.

422
Message at index <n> has empty content array. Must have at least one content part.

content is an empty array.

422
Message at index <n> has invalid content. Must be either a string or an array of content parts.

content is neither a string nor an array.

422
Message at index <n>: Content part at index <i> must be an object.

A content part is not an object.

422
Message at index <n>: Content part at index <i> must have a 'type' field.

A content part has no string type.

422
Message at index <n>: Content part at index <i> has invalid type '<type>'. Must be one of: input_text, output_text, input_image, output_slots, output_status.

type is not one of the five content part types.

422
Message at index <n>: Content part at index <i> with type '<type>' must have a 'text' property.

An input_text or output_text part has no string text.

422
Message at index <n>: Content part at index <i> with type '<type>' should not have '<field>' property.

A part carries a field that belongs to another type (for example image_url on an input_text part, or text on an output_slots part).

422
Message at index <n>: Content part at index <i> with type 'input_image' must have an 'image_url' string property.

An input_image part has no string image_url.

422
Message at index <n>: Content part at index <i> with type 'output_slots' must have a 'slots' array property.

An output_slots part has no slots array.

422
Message at index <n>: Content part at index <i>: slot at index <j> must be an object.

An entry of slots is not an object.

422
Message at index <n>: Content part at index <i>: slot at index <j> must have an 'id' string property.

A slot has no string id.

422
Message at index <n>: Content part at index <i> with type 'output_status' must have a 'status' string property.

An output_status part has no string status. Only reachable for parts that normalization did not rewrite.

422
Messages array cannot be empty.

messages is omitted or [].

422
datasetIds must be an array.

datasetIds is present but not an array (including null).

422
filters must be an object.

filters is present but is a string, number or boolean.

422
gender filter must be a string.

filters.gender is present but not a string.

422
Invalid datasetIds for this organization: <id>, <id>

One or more ids in datasetIds do not belong to the organization. Ids are trimmed and de-duplicated before the check.

502
Invalid AI response format.

Non-streaming only. The model finished but its output could not be parsed. In streaming mode the same text arrives as an error event.

502
Looks like I ran into some trouble generating your outfit. Let's try that again.

Non-streaming only. The model call failed mid-generation. In streaming mode the same text arrives as an error event. The placeholder assistant message stored in the chat is finalized with this text so the conversation stays valid.

503
Inventory index unavailable. Try again in a moment.

The catalogue facet index for the selected datasets has not been built yet or is being rebuilt. A rebuild is requested automatically; retry after a short delay.

curl -X POST "https://stylor.ai/api/v1/chat" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "messages": [
    {
      "role": "user",
      "content": "I need a smart-casual outfit for a rooftop dinner, budget around $300."
    }
  ],
  "filters": {
    "gender": "womens"
  }
}'
{
  "chatId": "3f0c2a4e-8b1d-4c6a-9e2f-7a5b1c0d9e88",
  "output": {
    "status": "resolved",
    "message": "For a rooftop dinner I'd pair a silk slip dress with a cropped leather jacket and strappy heels.",
    "slots": [
      {
        "id": "8066a68a-a39e-4d3e-993a-3887f6245032",
        "type": "search",
        "slot": "dress",
        "query": "black silk slip dress",
        "filters": {
          "fit": [
            "slim"
          ],
          "pattern": [
            "solid"
          ],
          "material": [
            "silk"
          ],
          "color_family": [
            "black"
          ],
          "color_rgb": [
            20,
            20,
            20
          ],
          "family": [
            "dress"
          ],
          "exclude_terms": [
            "maxi"
          ],
          "boost_terms": [
            "silk",
            "slip",
            "cowl"
          ],
          "minPrice": null,
          "maxPrice": 180
        },
        "results": []
      },
      {
        "id": "1d7c3b52-6e0f-4d2a-8a9b-2c4e6f8a0b13",
        "type": "search",
        "slot": "outerwear",
        "query": "cropped black leather jacket",
        "filters": {
          "fit": [
            "regular"
          ],
          "pattern": null,
          "material": [
            "leather"
          ],
          "color_family": [
            "black"
          ],
          "color_rgb": null,
          "family": [
            "jacket"
          ],
          "exclude_terms": [],
          "boost_terms": [
            "cropped",
            "moto"
          ],
          "minPrice": null,
          "maxPrice": 120
        },
        "results": []
      },
      {
        "id": "5b2e9f40-3c7d-4e1a-9f6b-8d0c2a4e6f17",
        "type": "outfit_compositions",
        "results": []
      }
    ]
  },
  "warnings": [
    {
      "message": "Dataset \"9c1a7d4e-2b5f-4a8c-b6e1-0d3f5a7c9e21\" is still in draft mode and may not have complete or production-ready data."
    }
  ]
}