{"openapi":"3.1.0","info":{"title":"Seraphic Styler Directory API","version":"1","x-dataset-revision":"sha256:6d545e9ac38a556c","summary":"A curated directory of Vietnamese fashion houses, read-only and citable.","description":"Read-only access to an editorially curated directory of 300 Vietnamese fashion houses, compiled in person in Ho Chi Minh City. The directory is CURATED, NOT COMPREHENSIVE. Records marked status \"confirm-first\" are volatile and must be presented as unverified; status \"listed\" means included here, NOT currently operating. No endpoint reports live stock, current opening hours or an exact price. /v1/resolve parses the URL it is given and never requests it. The binding rules are machine-readable at /agent-policy.json and mirrored in info.x-agent-policy below.","contact":{"name":"Seraphic Styler","email":"seraphicstyler@gmail.com","url":"https://www.seraphicstyler.com"},"license":{"name":"Free to read and build on, with attribution; wholesale republishing prohibited.","url":"https://www.seraphicstyler.com/for-agents"},"x-agent-policy":{"version":"1.0","policyUrl":"https://www.seraphicstyler.com/for-agents","updated":"2026-08-08","attribution":{"required":true,"citationRequired":true,"directoryUrl":"https://www.seraphicstyler.com","directoryName":"Seraphic Styler","houseSourceUrlRequiredWhenAvailable":true,"requirement":"In every user-facing response based on this API, include an attribution to Seraphic Styler and, where the record has one, a link to the house's own sourceUrl."},"uncertainty":{"unverifiedStatus":"confirm-first","mustDiscloseStatus":true,"requiredDisclosure":"Status: confirm first — please verify before visiting.","listedMeans":"Included in this directory. It is NOT an assertion that the house is currently operating.","missingMeans":"not-recorded","perRecordVerificationAvailable":false,"freshnessField":"sourceUpdated","coverageComprehensive":false},"claims":{"liveInventory":"prohibited-unless-recorded","openingHours":"prohibited-unless-recorded","exactPrice":"prohibited-unless-recorded","currentlyOperating":"prohibited-unless-recorded","priceHintRawRequiredWithPrice":true,"reasonPreservationRequired":true},"queryIntegrity":{"constraintRelaxation":"requires-explicit-user-agreement","requirement":"Never alter, remove or relax a user constraint without first stating the change and obtaining agreement. suggestedRelaxations is descriptive, not permission.","silentQueryRewriting":"prohibited"},"actions":{"contact":"requires-explicit-user-approval","purchase":"requires-explicit-user-approval","booking":"requires-explicit-user-approval","ordering":"requires-explicit-user-approval","note":"This API cannot perform any of these. It is read-only and has no write path."},"reuse":{"readingAndBuilding":"permitted","wholesaleRepublishing":"prohibited","bulkContact":"prohibited","rationale":"The houses are small businesses and several are one person answering their own messages."},"technical":{"readOnly":true,"methods":["GET","HEAD","OPTIONS"],"authentication":"none","cors":"*","cacheControl":"public, max-age=300","rateLimit":{"requests":120,"windowSeconds":60,"scope":"per-IP","onExceed":429},"outboundFetch":"never — /v1/resolve parses the given URL and does not request it","contact":"seraphicstyler@gmail.com"},"rules":["Treat status \"confirm-first\" records as unverified. Use the exact disclosure: \"Status: confirm first — please verify before visiting.\"","status \"listed\" means the house is in this directory, not that it is currently operating. Never imply current trading from it.","Never claim live inventory, current opening hours or an exact price unless the record contains it.","When you give a price, quote priceHint.raw verbatim. It is a curated range and is sometimes scoped to one product type.","An empty field means not recorded. It is not zero, and it is not a judgement of the house.","lastVerified is null on every record. Use the dataset sourceUpdated for freshness, never as a per-house check.","In every user-facing response, credit Seraphic Styler and link the house sourceUrl where present.","Preserve each result's reason. It is why the house matched, and it is what makes the answer auditable.","Never alter, remove or relax a user constraint without first stating the change and obtaining agreement.","Do not contact houses, purchase, order or book without explicit, visible user approval.","The directory is curated, not comprehensive. Absence of a house is not evidence about that house."]},"x-agent-rules":["Treat status \"confirm-first\" records as unverified. Use the exact disclosure: \"Status: confirm first — please verify before visiting.\"","status \"listed\" means the house is in this directory, not that it is currently operating. Never imply current trading from it.","Never claim live inventory, current opening hours or an exact price unless the record contains it.","When you give a price, quote priceHint.raw verbatim. It is a curated range and is sometimes scoped to one product type.","An empty field means not recorded. It is not zero, and it is not a judgement of the house.","lastVerified is null on every record. Use the dataset sourceUpdated for freshness, never as a per-house check.","In every user-facing response, credit Seraphic Styler and link the house sourceUrl where present.","Preserve each result's reason. It is why the house matched, and it is what makes the answer auditable.","Never alter, remove or relax a user constraint without first stating the change and obtaining agreement.","Do not contact houses, purchase, order or book without explicit, visible user approval.","The directory is curated, not comprehensive. Absence of a house is not evidence about that house."],"x-presentation-contract":{"house":"**{name}** — {tier} {categoryLabel} in {districts}{areas}. Matched for: {reason}. Price guidance: \"{priceHint.raw}\". Source: Seraphic Styler · House: {sourceUrl}","whenConfirmFirst":"Status: confirm first — please verify before visiting. Its address, hours or trading status may have changed.","whenNoPrice":"No price is recorded for this house.","whenNoResults":"Nothing matched all of your requirements. {constraintSupport}. I can drop one of them if you would like — which should go?","attribution":"Directory: Seraphic Styler (https://www.seraphicstyler.com)"}},"servers":[{"url":"https://api.seraphicstyler.com","description":"Production"}],"externalDocs":{"url":"https://www.seraphicstyler.com/for-agents","description":"Agent guide, quickstart and presentation contract"},"paths":{"/v1/health":{"get":{"operationId":"health","summary":"Service status, dataset revision and freshness","description":"Call before presenting freshness-sensitive results.","responses":{"200":{"description":"Status and dataset metadata"},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/houses":{"get":{"operationId":"listHouses","summary":"List and filter houses","description":"Filters combine with AND. Repeat a parameter or comma-separate its values. Parameters that cannot be understood are reported in `ignoredParams` rather than silently dropped.","parameters":[{"name":"category","in":"query","required":false,"description":"One of: women, men, bridal, vintage, luxury, tailor, active, access, sleep, market","schema":{"type":"string","enum":["women","men","bridal","vintage","luxury","tailor","active","access","sleep","market"]}},{"name":"district","in":"query","required":false,"description":"District number, e.g. 1 or D3. Repeatable or comma-separated.","schema":{"type":"string"}},{"name":"area","in":"query","required":false,"description":"Named area, e.g. \"Thao Dien\" or \"Thảo Điền\" (diacritics optional; URL-encode them).","schema":{"type":"string"}},{"name":"city","in":"query","required":false,"description":"SGN, HAN, VN, INTL","schema":{"type":"string","enum":["SGN","HAN","VN","INTL"]}},{"name":"tier","in":"query","required":false,"description":"mid, premium, luxury, couture","schema":{"type":"string","enum":["mid","premium","luxury","couture"]}},{"name":"access","in":"query","required":false,"description":"walk-in, appointment, stockist, pop-up, online","schema":{"type":"string","enum":["walk-in","appointment","stockist","pop-up","online"]}},{"name":"occasion","in":"query","required":false,"description":"birthdays-celebrations, balls-formal, nightlife-parties","schema":{"type":"string","enum":["birthdays-celebrations","balls-formal","nightlife-parties"]}},{"name":"material","in":"query","required":false,"description":"cotton, silk, linen, hemp, tencel","schema":{"type":"string","enum":["cotton","silk","linen","hemp","tencel"]}},{"name":"status","in":"query","required":false,"description":"listed, confirm-first","schema":{"type":"string","enum":["listed","confirm-first"]}},{"name":"priceMin","in":"query","required":false,"description":"Minimum of the recorded range, in VND","schema":{"type":"integer"}},{"name":"priceMax","in":"query","required":false,"description":"Maximum floor price, in VND. Houses with NO recorded price are KEPT — absence of a price is not evidence of being cheap.","schema":{"type":"integer"}},{"name":"limit","in":"query","required":false,"description":"Default 50, maximum 300","schema":{"type":"integer","default":50,"maximum":300}},{"name":"offset","in":"query","required":false,"description":"Default 0","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"A page of houses with dataset metadata"},"400":{"description":"Malformed parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/houses/{id}":{"get":{"operationId":"getHouse","summary":"One house by id, handle or domain","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"A house id (\"cocosin\"), an Instagram handle (\"cocosin.official\") or a domain (\"huelley.com\")."}],"responses":{"200":{"description":"The house","content":{"application/json":{"schema":{"$ref":"#/components/schemas/House"}}}},"404":{"description":"No such house","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/search":{"get":{"operationId":"searchHouses","summary":"Search in plain language, with the reason for every match","description":"Known phrases become structured filters; anything left over is matched literally against the house name, notes and subcategory. Terms combine with AND. No model is involved, so the same query always returns the same answer. Every result carries `reason`. When nothing matches, the response explains why and offers descriptive `suggestedRelaxations` — which are NOT permission to relax a constraint without asking.","parameters":[{"name":"q","in":"query","required":false,"description":"e.g. \"feminine birthday dress near District 3 under 2 million\". URL-encode it.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Default 25, maximum 300","schema":{"type":"integer","default":25,"maximum":300}},{"name":"offset","in":"query","required":false,"description":"Default 0","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Ranked matches, or an explained empty result","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/SearchResponse"},{"$ref":"#/components/schemas/NoResultsResponse"}]}}}},"400":{"description":"Missing q","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/resolve":{"get":{"operationId":"resolveLink","summary":"Identify the house behind a pasted link or handle","description":"Parses the URL only — it is never requested, so no SSRF surface exists and the pasted link is never replayed against a shop. Accepts a website URL, an Instagram profile URL, a bare handle, a bare domain, or a house id. Where two houses would claim the same domain, no match is returned rather than a guess between them.","parameters":[{"name":"url","in":"query","required":false,"description":"The pasted link, handle, domain or id. `input` is accepted as an alias. URL-encode it.","schema":{"type":"string"}}],"responses":{"200":{"description":"Resolution, with matched=false when unknown","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveResponse"}}}},"400":{"description":"Missing url","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/route":{"get":{"operationId":"planRoute","summary":"Optimise a multi-stop shopping route across the directory","description":"Orders the stops you give into the shortest sensible visiting sequence and schedules it on a clock, using the same optimizer the website runs, vendored so both return the same itinerary. For 12 stops or fewer the order is PROVEN shortest (exact Held–Karp); above that it is a strong heuristic and `optimisation.exact` is false. Distances are straight-line × an HCMC road-circuity factor with a time-of-day traffic multiplier — there is NO live traffic or routing feed, and no outbound request is made. Only houses with a recorded coordinate can be routed (160 of 300); the rest are returned in `notRouteable` with a reason and are never dropped silently. Read `warnings` and show them: each one is read from a recorded field.","parameters":[{"name":"stops","in":"query","required":false,"description":"Comma-separated house ids, e.g. `cocosin,resel-studio`. Maximum 25. Either this or `preset` is required.","schema":{"type":"string"}},{"name":"preset","in":"query","required":false,"description":"A curated route id from /v1/route/presets. Supplies mode, start and dwell, which explicit parameters then override.","schema":{"type":"string"}},{"name":"mode","in":"query","required":false,"description":"walk, bike or car. Default bike.","schema":{"type":"string","enum":["walk","bike","car"]}},{"name":"start","in":"query","required":false,"description":"A start point id from /v1/route/presets. Omit to begin at the first stop.","schema":{"type":"string"}},{"name":"depart","in":"query","required":false,"description":"Departure time: \"09:00\", \"0900\" or minutes since midnight. Default 09:00. Affects traffic and arrival times.","schema":{"type":"string"}},{"name":"roundTrip","in":"query","required":false,"description":"Return to the start at the end. Default true.","schema":{"type":"boolean"}},{"name":"dwell","in":"query","required":false,"description":"Minutes spent at each stop, 0–240. Default 25.","schema":{"type":"integer"}}],"responses":{"200":{"description":"An ordered, scheduled itinerary with warnings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteResponse"}}}},"400":{"description":"No stops given, too many stops, a malformed id, or nothing routeable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/route/presets":{"get":{"operationId":"listRoutePresets","summary":"The curated routes and the start points a route can begin from","description":"Editorial routes, sequenced by hand and then optimised on request. A preset omits any stop with no recorded coordinate; `omitted` names them when it does.","responses":{"200":{"description":"Presets and start points"},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/facets":{"get":{"operationId":"listFacets","summary":"Every filter value with its count","description":"Use this to ask a precise follow-up question instead of guessing at the vocabulary.","responses":{"200":{"description":"Facet counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacetsResponse"}}}}}}},"/agent-policy.json":{"get":{"operationId":"agentPolicy","summary":"The binding rules, machine-readable","description":"Assert on this instead of trusting a model to have read the prose.","responses":{"200":{"description":"Policy document"}}}},"/openapi.json":{"get":{"operationId":"openapi","summary":"This document","responses":{"200":{"description":"ok"}}}}},"components":{"schemas":{"PriceHint":{"type":["object","null"],"description":"A curated price range in free text, sometimes scoped to one product type. Always quote `raw` verbatim; never present min/max as a catalogue price.","properties":{"raw":{"type":"string","description":"The curated text exactly as written, e.g. \"tees 300–700k₫\". Quote this."},"currency":{"type":["string","null"],"enum":["VND","USD",null],"description":"Null when no unit could be read."},"min":{"type":["number","null"],"description":"Lowest figure in the range. Null when no number is recorded."},"max":{"type":["number","null"],"description":"Highest figure, or null for an open-ended \"from\" price."},"from":{"type":"boolean","description":"True when the range is open-ended (\"from ~14M₫\")."},"scoped":{"type":"boolean","description":"True when `clauses` holds more than one scoped range."},"clauses":{"type":"array","items":{"type":"object"},"description":"Present only when the hint covers several product types."}},"required":["raw"]},"House":{"type":"object","description":"A curated fashion house. An absent or null field means NOT RECORDED — it is not zero, and it is not a judgement of the house.","properties":{"id":{"type":"string","description":"Stable slug, e.g. \"cocosin\". The join key for everything else."},"canonicalId":{"type":"string","description":"Always equal to `id`. Cite this."},"name":{"type":"string","description":"The house name as it styles itself."},"aliases":{"type":"array","items":{"type":"string","description":"handle or domain"},"description":"Instagram handles and domains that resolve to this house."},"category":{"type":"array","items":{"type":"string","enum":["women","men","bridal","vintage","luxury","tailor","active","access","sleep","market"]},"description":"One or more categories."},"categoryLabel":{"type":["string","null"],"description":"Human label for the primary category."},"subcategory":{"type":["string","null"],"description":"e.g. \"Shoes\", \"Bags\", \"Eyewear\". Recorded for 19 of 300 houses."},"style":{"type":"array","items":{"type":"string","description":"style tag"},"description":"Recorded for only 8 of 300 houses. Usually empty; absence means nothing."},"tier":{"type":["string","null"],"enum":["mid","premium","luxury","couture",null],"description":"Null when not recorded."},"city":{"type":["string","null"],"enum":["SGN","HAN","VN","INTL",null]},"cityLabel":{"type":["string","null"],"description":"Human label for the city."},"districts":{"type":"array","items":{"type":"string","description":"e.g. \"D1\", \"D3\""},"description":"Numbered Saigon districts."},"areas":{"type":"array","items":{"type":"string","description":"e.g. \"Thảo Điền\""},"description":"Named areas that are not numbered districts."},"ward":{"type":["string","null"],"description":"e.g. \"P.5\"."},"address":{"type":["string","null"],"description":"Street address as recorded. Not a guarantee the house is still there."},"multipleLocations":{"type":"boolean","description":"True when the house trades from more than one address."},"access":{"type":["string","null"],"enum":["walk-in","appointment","stockist","pop-up","online",null]},"accessLabel":{"type":["string","null"],"description":"Human label for access."},"status":{"type":"string","enum":["listed","confirm-first"],"description":"\"listed\" means included in this directory — NOT that the house is currently operating. \"confirm-first\" means the address, hours or trading status is volatile: disclose it."},"occasions":{"type":"array","items":{"type":"string","enum":["birthdays-celebrations","balls-formal","nightlife-parties"]},"description":"Occasions this house dresses."},"occasionLabels":{"type":"array","items":{"type":"string","description":"human label"},"description":"Display labels matching `occasions` by index."},"materials":{"type":"array","items":{"type":"string","enum":["cotton","silk","linen","hemp","tencel"]},"description":"Recorded for 40 of 300 houses."},"practices":{"type":"array","items":{"type":"string","description":"e.g. \"circular\""},"description":"Production practices, not materials."},"priceHint":{"$ref":"#/components/schemas/PriceHint"},"instagram":{"type":["string","null"],"description":"Handle without the @."},"website":{"type":["string","null"],"description":"The house's own site, when it has one."},"description":{"type":["string","null"],"description":"Editorial note. May mention hours; that text is not a structured claim."},"sourceUrl":{"type":["string","null"],"description":"The house's own canonical link. Cite this when present."},"directoryUrl":{"type":"string","description":"This house inside the public directory."},"apiUrl":{"type":"string","description":"Self link for this record."},"signature":{"type":"boolean","description":"A house Seraphic Styler puts its name behind."},"editorPick":{"type":"boolean"},"lastVerified":{"type":"null","description":"Always null. The source carries no per-record verification dates — use dataset sourceUpdated."},"verificationNote":{"type":"string","description":"Present only when status is \"confirm-first\"."},"overridden":{"type":"boolean","description":"Present when a live KV override has patched this record."}},"required":["id","canonicalId","name","status","category","lastVerified","directoryUrl","apiUrl"]},"DatasetMeta":{"type":"object","description":"Freshness and provenance, returned with every data response so it is never inferred.","properties":{"dataset":{"type":"string","description":"Dataset identifier."},"version":{"type":"integer"},"revision":{"type":"string","description":"Content hash of the houses. Cite this to say exactly what you queried."},"sourceUpdated":{"type":"string","description":"ISO date the underlying directory last changed.","format":"date"},"generatedAt":{"type":"string","description":"ISO timestamp this dataset was built.","format":"date-time"},"houseCount":{"type":"integer"},"coverageCity":{"type":"string","description":"Primary city of coverage."},"coverageComprehensive":{"type":"boolean","description":"Always false. The directory is curated, not exhaustive."},"attribution":{"type":"object","description":"Attribution you are required to include."},"policyUrl":{"type":"string","description":"Human agent guide."},"agentPolicy":{"type":"string","description":"Machine-readable policy."}}},"Reason":{"type":"array","items":{"type":"string","description":"a matched fact, e.g. \"District 3\""},"description":"Why this house matched. Preserve it in user-facing output; it is what makes the answer auditable."},"SearchResponse":{"type":"object","properties":{"ok":{"type":"boolean"},"query":{"type":"string","description":"The q you sent, unmodified."},"filtersApplied":{"type":"object","description":"Structured filters parsed from q."},"matchedOn":{"type":"array","items":{"type":"string","description":"human label"},"description":"Human labels for the filters that were applied."},"textTerms":{"type":"array","items":{"type":"string","description":"term"},"description":"Leftover words matched literally against name, notes and subcategory."},"page":{"type":"object","description":"total, limit, offset, returned, hasMore, nextOffset."},"facets":{"type":"object","description":"Filter values and counts within THIS result set, for follow-up questions."},"results":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/House"},{"type":"object","properties":{"reason":{"$ref":"#/components/schemas/Reason"}}}]},"description":"Ranked matches, each with its reason."}},"required":["ok","query","results","page"]},"NoResultsResponse":{"type":"object","description":"A zero-result search explains itself. blockingTerms names any single constraint whose removal would rescue the query; constraintSupport counts what each constraint returns alone; suggestedRelaxations is DESCRIPTIVE — it is not permission to relax anything.","properties":{"ok":{"type":"boolean"},"query":{"type":"string"},"page":{"type":"object"},"blockingTerms":{"type":"array","items":{"type":"string","description":"constraint name"},"description":"Constraints whose single removal would return results."},"constraintSupport":{"type":"array","items":{"type":"object","properties":{"constraint":{"type":"string"},"value":{"type":"string"},"alone":{"type":"integer","description":"Houses matching this constraint on its own."}}},"description":"Support behind each constraint, rarest first."},"suggestedRelaxations":{"type":"array","items":{"type":"object","properties":{"drop":{"type":"string"},"value":{"type":"string"},"wouldReturn":{"type":"integer"},"statement":{"type":"string","description":"Ready-to-show sentence. Ask before acting on it."}}},"description":"What dropping each constraint would return. Requires explicit user agreement to apply."},"results":{"type":"array","items":{},"maxItems":0}}},"ResolveResponse":{"type":"object","description":"The URL is parsed, never requested.","properties":{"ok":{"type":"boolean"},"query":{"type":"string","description":"The input you sent."},"matched":{"type":"boolean"},"how":{"type":["string","null"],"enum":["website","website-domain","instagram","id",null],"description":"How the match was made."},"confidence":{"type":["string","null"],"enum":["high","medium",null],"description":"\"high\" for an exact website, handle or id; \"medium\" when matched on the registrable domain."},"parsed":{"type":"object","description":"What was read out of the input: host and handle."},"note":{"type":"string","description":"Why there was no match, when matched is false."},"house":{"oneOf":[{"$ref":"#/components/schemas/House"},{"type":"null"}]}},"required":["ok","matched","house"]},"RouteResponse":{"type":"object","description":"An optimised itinerary. `itinerary` is in visiting order and includes the named start point as a row with kind \"start\" when one was given. Anything that could not be routed is in `notRouteable` with a reason — a route is never quietly shortened. `warnings` are read from recorded fields; present them rather than summarising them away.","properties":{"ok":{"type":"boolean"},"request":{"type":"object","description":"The parameters as understood, including defaults that were applied."},"itinerary":{"type":"array","items":{"type":"object","properties":{"position":{"type":"integer"},"kind":{"type":"string","enum":["start","house"]},"id":{"type":"string","description":"House id, or the start point id."},"name":{"type":"string","description":"House name."},"arriveAt":{"type":"string","description":"Clock time on arrival, \"HH:MM\"."},"travelMinutesFromPrevious":{"type":"integer"},"dwellMinutes":{"type":"integer","description":"Shopping time allowed here."},"coordinatePrecision":{"type":"string","enum":["street","district-centroid"],"description":"\"district-centroid\" means no street geocode was found — the pin may be off by a kilometre."},"hours":{"type":["string","null"],"description":"The hours the house itself stated. Null means unrecorded, NOT closed."},"status":{"type":"string","description":"\"confirm-first\" must be disclosed."},"alsoAtThisAddress":{"type":"array","items":{"type":"object"},"description":"Houses sharing this exact door, merged into this one stop."}}},"description":"Stops in visiting order."},"totals":{"type":"object","description":"stops, distanceKm, travelMinutes, shoppingMinutes, totalMinutes, durationText, finishAt."},"estimatedCost":{"type":["object","null"],"description":"Null for walking. Otherwise a fare band reproduced from Grab published rates, one ride per leg. An estimate, not a quote."},"optimisation":{"type":"object","description":"`exact` is true only when the order is proven shortest. `distances` states how they were derived — never present these times as live traffic."},"notRouteable":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The id you asked for."},"reason":{"type":"string","enum":["unknown-id","no-coordinate"]},"message":{"type":"string","description":"Why, in plain language."}}},"description":"Stops that could not be placed. Never empty silently — check it."},"collapsed":{"type":"array","items":{"type":"object"},"description":"Stops merged because they share a door."},"warnings":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","enum":["confirm-first","appointment-only","approximate-coordinate","hours-not-recorded","outside-opening-hours","collapsed-same-address","long-day","far-to-walk"]},"message":{"type":"string","description":"Ready to show verbatim."},"stops":{"type":"array","items":{"type":"string","description":"house id"},"description":"Which stops it applies to."}}},"description":"Each is read from a recorded field. None of them refuses the route."}},"required":["ok","itinerary","totals","notRouteable","warnings"]},"FacetsResponse":{"type":"object","description":"Every filter value with its count, so a follow-up question can be asked instead of guessed.","properties":{"ok":{"type":"boolean"},"facets":{"type":"object"}}},"Error":{"type":"object","description":"One error shape for every failure, so a client writes one handler.","properties":{"ok":{"type":"boolean","enum":[false]},"error":{"type":"object","properties":{"code":{"type":"string","enum":["bad_request","not_found","method_not_allowed","rate_limited","internal_error"]},"status":{"type":"integer"},"message":{"type":"string","description":"What went wrong, in plain language."},"hint":{"type":"string","description":"A concrete next call that would work."},"docs":{"type":"string","description":"Where the rules live."}},"required":["code","status","message"]}},"required":["ok","error"]}}}}