--- title: "0.1.0 - DesignWithMe SDK" slug: "0-1-0-designwithme-sdk" updated: 2026-09-11T17:27:05Z published: 2026-09-11T17:27:05Z canonical: "developers.3dcloud.com/0-1-0-designwithme-sdk" --- > ## Documentation Index > Fetch the complete documentation index at: https://developers.3dcloud.com/llms.txt > Use this file to discover all available pages before exploring further. # 0.1.0 - DesignWithMe SDK *Applies to `@mxt/designwithme-sdk` **v0.1.0-epic-PROD-8598.0**.* #### DRAFT**** ## Overview **AI design services for your website.** One API key in, events out: image generation, product recommendations, price estimation, and a design assistant that drives the first two — behind a single simple client. ``` import { createDesignWithMe } from '@mxt/designwithme-sdk'; const dwm = createDesignWithMe({ apiKey: 'your-api-key', environment: 'development' }); ``` That is the whole setup. The first call signs in automatically; there is no login step, no token to store, and nothing to refresh. [Get Started](/designwithme-ai/docs/0-1-0-designwithme-sdk#getting-started) · [SDK Reference](/designwithme-ai/docs/0-1-0-designwithme-sdk#image-generation) · [Live Examples](/designwithme-ai/docs/0-1-0-designwithme-sdk#examples) ### How the pieces fit Everything hangs off one object. `createDesignWithMe` returns a **client** — conventionally `dwm` — and that client owns the session, the event bus, and all the state. Under it sit four service namespaces, one per AI service: ``` createDesignWithMe({ apiKey, environment }) │ ▼ ┌───────────────────────────────────────────────────────────────┐ │ dwm — DesignWithMeClient one per application │ │ the session · the event bus · the activity log │ └───────────────────────────────────────────────────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ dwm.images dwm.recommendations dwm.estimates dwm.chat │ │ │ │ ▼ ▼ ▼ ▼ GenerationJob RecommendationResult PriceEstimate ChatMessage[] a live handle: SearchResult a plain value: ChatAction[] progress, ranked SKUs areas, a cost a streamed reply result(), + match scores range, a plus the actions refreshImages() breakdown it triggered ``` The assistant is not a fifth service — it is a caller of the other ones. When a conversation calls for a render or a product, the SDK runs that service call for you and reports it as a `chat:action`: ``` dwm.chat ──triggers──▶ dwm.images 'imageGeneration' action ──triggers──▶ dwm.recommendations 'recommendations' action ─────✗──────▶ dwm.estimates never — see below ``` Price estimation is a direct API only. The assistant is not offered it as a tool, so no `chat:action` will ever carry one. #### There is no "project" object The SDK has no project, room, or design entity that the services hang off — nothing to create first, nothing to keep in sync. What ties calls together is **your own data**: the room photo you host and the SKUs from your catalog. You pass them to whichever service you want, in any order. ``` a room photo URL ─┬─▶ dwm.images.generate({ roomImageUri, products }) (yours, public) ├─▶ dwm.estimates.create({ roomImageUri, roomType }) └─▶ dwm.chat.send(text, { attachments }) catalog SKUs ─┬─▶ dwm.images.generate({ products }) (yours) └─▶ dwm.recommendations.forProducts({ skus }) ``` `projectId` on an image-generation request is an optional grouping *tag* on the job record — a string you choose, for your own reporting. It creates nothing, and no other service reads it. ### A typical session What a real end-user flow looks like end to end, and where the SDK does work on your behalf: ``` page load │ const dwm = createDesignWithMe({ apiKey, environment }) │ nothing on the wire yet — creating a client is free ▼ first call │ the SDK mints a session from your key, once, silently │ and renews it in the background from then on ▼ user gives you │ dwm.estimates.create({ roomImageUri, roomType }) a room photo │ → PriceEstimate: areas, cost range, confidence │ dwm.recommendations.forProducts({ skus }) │ → ranked SKUs from your catalog, with scores ▼ user picks │ const job = dwm.images.generate({ roomImageUri, products }) products │ job.on('progress', …) ← narrate the wait │ await job.result() → the generated scene ▼ user talks to │ await dwm.chat.start(); dwm.chat.send('warmer, please') the assistant │ chat:delta → the reply streams in, token by token │ chat:action → the SDK runs a generation or a │ recommendation and reports each step ▼ user refreshes │ dwm.images.resume(jobId) — the job never stopped the page │ dwm.chat.resume(chatId) — the transcript comes back ▼ user returns │ dwm.chat.list() → their past conversations next week │ job.refreshImages() → fresh URLs for old scenes ``` Two things to take from that. **You never wait on the SDK to be ready** — there is no connect step, and the session is established on the first call that needs it. And **the work outlives the page**: generation jobs and chats live on the service, so a refresh is a `resume`, not a restart. ### 🔑 One key, zero plumbing Initialize with your API key. Sessions, token renewal, retries, and reconnects are the SDK's problem — never yours. [Read more](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication) ### 📡 One event model everywhere Every action reports the same way — subscribe with `dwm.on()`, await the promise, or read the live state. Success and error handling you learn once. [Read more](/designwithme-ai/docs/0-1-0-designwithme-sdk#events) ### 🖼️ Image Generation A room photo and a product list in, photorealistic scenes out. Progress events while it works, a promise when it's done, resumable after a page refresh. [Read more](/designwithme-ai/docs/0-1-0-designwithme-sdk#image-generation) ### 🛋️ Product Recommendations Ranked, style-matched products from a cart or a free-text description — one call, one score scale. [Read more](/designwithme-ai/docs/0-1-0-designwithme-sdk#product-recommendations) ### 💰 Price Estimation Room photo in, budget out — areas, cost ranges, per-component breakdowns, and confidence scores. Imperial and metric. [Read more](/designwithme-ai/docs/0-1-0-designwithme-sdk#price-estimation) ### 💬 A design assistant that acts The chat assistant doesn't just talk — it generates images and finds products mid-conversation. The SDK runs those actions for you and tells you about each step. [Read more](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat) ### Service availability The SDK is complete; the environments it can reach are not all deployed yet. [Getting Started](/designwithme-ai/docs/0-1-0-designwithme-sdk#getting-started-choosing-an-environment) carries the current status — check it before you plan a rollout. --- ## Getting Started This guide takes you from zero to your first working DesignWithMe integration: install the SDK, create a client, and get product recommendations — in about ten lines. The SDK is a **complete client** for the DesignWithMe AI services. You initialize it with your API key and call plain methods with plain objects; authentication, session management, request signing, polling, and WebSocket handling all happen inside. You never construct a URL, manage a token, or parse a wire payload. ### Prerequisites - **A DesignWithMe API key**, issued by 3D Cloud. Keys are provisioned per client — contact your account representative to request one. - **Node.js 18 or later**, or any modern bundler/browser environment. - TypeScript is optional but recommended — every method, option, event, and result is fully typed. ### Installation The package is served from the 3D Cloud npm registry, so first point the `@mxt` scope at it. Add this to the `.npmrc` in your project root (or your user-level `~/.npmrc`): ``` @mxt:registry=https://nexus.3dcloud.io/repository/npm-mxt/ ``` Then install: **npm** ``` npm install @mxt/designwithme-sdk ``` **pnpm** ``` pnpm add @mxt/designwithme-sdk ``` **yarn** ``` yarn add @mxt/designwithme-sdk ``` The package ships tree-shakeable ESM modules plus a minified single-file bundle (`dist/designwithme-sdk.min.js`) for direct `<script type="module">` usage. Type declarations are included for both. ### Create the client Everything starts with `createDesignWithMe`: ``` import { createDesignWithMe } from '@mxt/designwithme-sdk'; const dwm = createDesignWithMe({ apiKey: 'your-api-key', environment: 'development', }); ``` That's the whole setup. The first call you make signs in automatically; the SDK keeps the session alive from then on. There is no login step, no token to store, nothing to refresh. > **Warning: Protect your API key** > > > Your API key identifies your account. A key used from a browser is visible to anyone who opens dev tools — that is expected, and it is why browser keys are bound to an origin allowlist (see [Authentication](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication-the-origin-allowlist)). Load the key from environment configuration rather than committing it, and don't reuse a key across environments or across trust boundaries — [how many keys you need](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication-how-many-keys-do-i-need) covers the shape. ### Your first call Ask for products that pair well with a sofa: ``` const { products } = await dwm.recommendations.forProducts({ skus: ['SOFA-9412'], strategy: 'complementary', limit: 5, }); for (const match of products) { console.log(`${match.sku} — ${(match.score * 100).toFixed(0)}% match`); } ``` Every SDK result is a plain typed object. Every SKU here comes from **your own catalog** — your API key scopes everything to your account. ### Listening instead of awaiting Everything you can `await`, you can also observe. The SDK emits a named event for every action it takes, which is how UIs stay live without threading promises through components: ``` const off = dwm.on('recommendations:completed', ({ products }) => { renderCarousel(products); }); dwm.on('error', (error) => { toast(error.message); }); // later — every subscription returns its own unsubscribe off(); ``` Promises and events always agree: a call that rejects also emits its `:failed` event and the global `error` event. Pick whichever fits the moment — or use both. The full model is on the [Events & State](/designwithme-ai/docs/0-1-0-designwithme-sdk#events) page. ### Something longer-running: generate an image Image generation takes seconds, not milliseconds, so it returns a **job handle** you can watch: ``` const job = dwm.images.generate({ roomImageUri: 'https://example.com/uploads/living-room.jpg', products: [ { id: 'SOFA-9412', quantity: 1 }, { id: 'CHAIR-2210', quantity: 2 }, ], prompt: 'Warm evening lighting, keep the rug visible', }); job.on('progress', ({ status }) => showSpinner(status)); const images = await job.result(); render(images[0].url); ``` The handle tracks the job for you — submission, progress, completion — and `dwm.images.jobs` always holds the live list. See [Image Generation](/designwithme-ai/docs/0-1-0-designwithme-sdk#image-generation). ### Choosing an environment **The rule, permanently:** pass the environment your key was issued for. Keys are environment-specific and never work across one — a `development` key authenticates only against `development`. `environment` defaults to `'production'`, so any other environment must be named explicitly. | Environment | Purpose | | --- | --- | | `development` | Integration and testing | | `cert` | Pre-production validation | | `production` | Live traffic (the default) | Each environment is fully isolated: separate keys, separate catalogs, separate sessions. Nothing crosses over, so promoting an integration from one to the next is a key change and an `environment` change, and nothing else. #### Current rollout status This is a **status report, not a recommendation** — it says which environments exist today, not which one your integration should target. Once an environment is deployed and you hold a key for it, point at it. | Environment | Status as of this release | | --- | --- | | `development` | Deployed. | | `cert` | Not yet deployed. | | `production` | Not yet deployed. | Ask your account representative where things stand before you plan a production cutover; this page is only current as of the SDK version stamped at the top. ### Where to next - [Authentication](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication) — what the SDK does with your key, and the auth-related errors you can encounter. - [Events & State](/designwithme-ai/docs/0-1-0-designwithme-sdk#events) — the full event map and the queryable state that backs it. - [Interactive Chat](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat) — the design assistant, including how it triggers the other services for you. - [Integration Guide](/designwithme-ai/docs/0-1-0-designwithme-sdk#integration) — production patterns: framework bindings, resuming work after a refresh, error UX. - [Examples](/designwithme-ai/docs/0-1-0-designwithme-sdk#examples) — runnable pages for every service; add `?mock=1` to run one without a key. --- ## Authentication You give the SDK an API key. That's the entire integration surface of authentication — everything below explains what the SDK does with it, what's configured on your account, and which errors can surface. There are no endpoints to call, no tokens to store, and no refresh logic to write. ``` const dwm = createDesignWithMe({ apiKey: 'your-api-key', environment: 'development', }); ``` Keys are issued per client **and per environment**, so the two options above always travel together. Which environments are deployed today is a [rollout status](/designwithme-ai/docs/0-1-0-designwithme-sdk#getting-started-current-rollout-status), not a rule about which one to use: point at the environment your key was issued for. ### What the SDK does for you On your first call, the SDK exchanges your API key for a short-lived session, then manages that session for the lifetime of the client: - **Signs in lazily** — nothing happens at `createDesignWithMe`; the session is established on the first call that needs it, and concurrent first calls share one sign-in. - **Renews early** — the session is renewed in the background at ~80% of its lifetime, so calls never pause to authenticate mid-flow. - **Recovers automatically** — if a call is ever rejected as expired, the SDK re-authenticates and retries it once before reporting an error. - **Keeps chat flowing** — every chat turn opens its own authenticated stream, so a long conversation picks up fresh credentials between turns without any involvement from you. None of this is observable unless it fails — and failures surface the same way as every other SDK error (see below). Concretely: your key is sent once, as an `X-API-Key` header, to the DesignWithMe auth service. What comes back is a short-lived session token plus a refresh token, and every subsequent service call carries the session token instead of your key. The key itself is never attached to an image-generation, recommendation, estimate, or chat request. ### API keys Keys are provisioned per client by 3D Cloud — contact your account representative. Each key is bound to: | Setting | What it controls | | --- | --- | | **Environment** | The key works only in the environment it was issued for. | | **Origin allowlist** | Browser calls are accepted only from your registered web origins (scheme + host + port). | | **Scopes** | Which services the key may use — assigned server-side when the key is issued. | | **Active status** | Keys can be deactivated by 3D Cloud or on request; an inactive key stops authenticating immediately. | #### How many keys do I need? The unit is **one key per client account, per environment**. A single key covers every service your account is scoped for, and one key is enough for one application in one environment: ``` your account ├── development key ─── your staging site + your local dev origins ├── cert key ─── your pre-production site └── production key ─── your live site ``` You need a *second* key inside one environment only when a second surface has genuinely different exposure — most commonly a server-side integration alongside a browser one: | Surface | Why a separate key helps | | --- | --- | | Browser (your website) | The key ships in your JavaScript and is readable by anyone. It is protected by the origin allowlist, which only applies to browser requests. | | Server (your backend) | Requests carry no browser `Origin`, so the allowlist does not apply. A key used here is a genuine secret and should be treated as one. | Because a server key is not origin-bound, leaking one into shipped JavaScript hands out an unrestricted key. If you integrate on both sides, ask for one key for each so they can be rotated and revoked independently. If you only integrate in the browser, one key is the whole story — you are not expected to juggle several per app. #### Where to keep your key - **Browser key** — inject it at build or runtime from environment configuration (`VITE_DWM_API_KEY`, an app-config endpoint, a template variable). It ends up in your bundle and that is fine; the origin allowlist is what constrains it. Don't hard-code it in a committed source file, so rotation is a config change. - **Server key** — your existing secret store (environment variables, Secret Manager, Vault). Never send it to the browser and never put it in a public repository. - **Neither** — end users never see or hold a key. The SDK does not authenticate *your users*; it authenticates *your application*. If you need per-user identity, that stays in your own system. #### Scopes Scopes are granted per service (image generation, recommendations, price estimation, chat). You don't request scopes in code — they're fixed server-side on the key, returned on the session, and enforced by the services. Calling a service your key isn't scoped for fails with the error code `forbidden`. If a service you expect to use is rejected, ask your account representative to check the key's scopes. #### The origin allowlist For browser integrations, the allowlist is the control that makes shipping an API key in front-end code acceptable: a key presented from an unregistered origin is refused, regardless of whether the key itself is valid. **Where it is enforced:** in the DesignWithMe auth service, at the moment your key is exchanged for a session. The service compares the browser's `Origin` header against the list registered on that key and refuses with `403 ORIGIN_NOT_ALLOWED`. It is a property of the key, not a network appliance in front of it — there is no WAF rule, CDN configuration, or firewall entry involved, and nothing to configure on your side beyond telling us which origins to register. **How origins get registered:** by 3D Cloud, per key, on request — send your account representative the exact origins. There is no self-service screen today. Practical consequences: - Register every origin you serve from, including local development origins (e.g. `http://localhost:5173`) and any preview or staging hostnames. - Origins match exactly — scheme, host, and port all count. `https://shop.example.com` does not cover `https://www.shop.example.com`, and `http://localhost:5173` does not cover `:5174`. - Register origins ahead of a launch. A new hostname that nobody told us about fails closed. - Server-side (Node) calls send no browser `Origin` and are not subject to the allowlist — see [How many keys do I need?](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication-how-many-keys-do-i-need). > **Warning: A rejected origin usually surfaces as a network error** > > > The refusal is a `403` on a cross-origin request, so the browser blocks the response before the SDK can read it. In practice your code sees `network_error`, not `origin_not_allowed` — and the browser console shows a CORS failure. If a deployed site fails at the very first call with `network_error` while the same key works elsewhere, check the allowlist before you check your connectivity. ### Authentication errors Auth failures are ordinary [`DesignWithMeError`](/designwithme-ai/docs/0-1-0-designwithme-sdk#events-errors)s — they reject the call that triggered them and emit the global `error` event, like every other failure. | Code | Meaning | What to do | | --- | --- | --- | | `invalid_api_key` | The key is missing, malformed, revoked, or wrong for this environment | Check the key and the `environment` option together | | `origin_not_allowed` | The page's origin isn't on the key's allowlist | Register the origin with your account representative. From a browser this usually arrives as `network_error` — see the note above | | `client_inactive` | The client account is deactivated | Contact your account representative | | `forbidden` | The key lacks the scope for the service you called | Have the key's scopes checked | | `rate_limited` | Too many authentication attempts | Back off and retry; the SDK does not hammer sign-in | | `service_unavailable` | The service can't authenticate right now | Retry later | | `network_error` | The service was unreachable — or a cross-origin refusal the browser hid | Check the allowlist first, then connectivity | ``` import { isDesignWithMeError } from '@mxt/designwithme-sdk'; try { await dwm.recommendations.search({ query: 'walnut credenza' }); } catch (error) { if (isDesignWithMeError(error) && error.code === 'invalid_api_key') { // configuration problem — not retryable } } ``` A misconfigured key fails fast and consistently: the first call rejects, and every subsequent call rejects the same way until the configuration is fixed. The SDK never caches a failed sign-in. ### Security best practices - **Rely on the origin allowlist** for browser keys — it's the mechanism designed for front-end use. Keep the allowlist tight; don't register origins you don't control. - **Keep server keys out of the client.** A server key is not origin-bound, so it is a real secret. If you integrate on both sides, use a separate key per side rather than shipping one key everywhere — see [How many keys do I need?](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication-how-many-keys-do-i-need). - **Don't commit keys.** Load them from environment configuration at build or runtime. - **One client per application.** Create a single `dwm` instance and share it — it holds the session. Creating clients per-call throws away the managed session each time and re-authenticates needlessly. - **Rotate on suspicion.** If a key leaks, have it deactivated and reissued; deactivation takes effect immediately. ### What's next - [Events & State](/designwithme-ai/docs/0-1-0-designwithme-sdk#events) — how every SDK action, including auth failures, reports. - [Configuration Reference](/designwithme-ai/docs/0-1-0-designwithme-sdk#configuration) — every option `createDesignWithMe` accepts. --- ## Events & State Every action the SDK takes reports through **one model**: it returns a promise, it emits named events, and it updates queryable state. All three always agree — the event payload is the same object you can read from state, and a rejected promise always has a matching `:failed` event. Learn the model once and every service works the same way. ``` // 1. Await it… const images = await dwm.images.generate(request).result(); // 2. …or subscribe to it… dwm.on('generation:completed', ({ images }) => render(images)); // 3. …or read it later. const finished = dwm.images.jobs.filter((job) => job.status === 'complete'); ``` ### Subscribing `dwm.on(event, handler)` registers a typed handler and returns an **unsubscribe function** — designed to drop straight into framework cleanup: ``` // React useEffect(() => dwm.on('chat:message', (message) => setMessages([...dwm.chat.messages])), []); // anywhere const off = dwm.on('generation:progress', updateSpinner); off(); // done listening ``` - `dwm.on(event, handler)` — subscribe; returns `() => void`. - `dwm.once(event, handler)` — fire at most once; returns `() => void`. - `dwm.off(event, handler)` — remove a specific handler (the unsubscribe return is usually more convenient). - `dwm.emitted(event)` — a `Promise` of the event's next payload; handy for one-shot waits: ``` await dwm.emitted('chat:ready'); // resolves when the assistant is ready for input ``` Handlers are fully typed — the payload type is inferred from the event name. Throwing inside a handler never breaks the SDK or other handlers. ### The event map Names are namespaced `domain:action`. Facts are past tense (`:completed`, `:failed`); ongoing work reports as `:progress`. #### Image generation | Event | Payload | When | | --- | --- | --- | | `generation:submitted` | `GenerationJob` | The job was accepted and has an `id` | | `generation:progress` | `GenerationJob` | The job's status advanced (`queued` → `generating`) | | `generation:completed` | `GenerationJob` | Images are ready (`job.images` populated) | | `generation:failed` | `GenerationJob` | The job failed (`job.error` populated) | #### Recommendations & search | Event | Payload | When | | --- | --- | --- | | `recommendations:completed` | `RecommendationResult` | `forProducts` resolved | | `recommendations:failed` | `{ error }` | `forProducts` rejected | | `search:completed` | `SearchResult` | `search` resolved | | `search:failed` | `{ error }` | `search` rejected | #### Price estimation | Event | Payload | When | | --- | --- | --- | | `estimate:completed` | `PriceEstimate` | `create` resolved | | `estimate:failed` | `{ error }` | `create` rejected | #### Chat Ships with the design assistant — the [Interactive Chat](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat) page tracks its contract status. | Event | Payload | When | | --- | --- | --- | | `chat:statusChanged` | `{ current, previous, reason? }` | Any conversation-status transition | | `chat:ready` | `{ chatId }` | The active chat is ready for input | | `chat:failed` | `{ error }` | The active chat can't currently converse | | `chat:delta` | `{ messageId, delta, text }` | A fragment of a streaming assistant reply (`text` is the accumulated snapshot) | | `chat:message` | `ChatMessage` | A message was added to the transcript (user, or a completed assistant reply) | | `chat:action` | `ChatAction` | An assistant-triggered action changed status — see [Interactive Chat](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat-assistant-actions) | #### Global | Event | Payload | When | | --- | --- | --- | | `error` | `DesignWithMeError` | Any operation failed, anywhere — one tap for all failure logging | | `activity` | `ActivityEntry` | Anything happened — the umbrella feed of every event above | ### Promises and events always agree The rules, which hold everywhere: 1. **Every operation returns a promise** that resolves with the result or rejects with a `DesignWithMeError`. Nothing fails silently. 2. **Every outcome also emits**: success emits the domain's `:completed` event; failure emits its `:failed` event *and* the global `error` event. 3. **Events are additive, not alternative.** Awaiting a promise doesn't suppress its events; subscribing doesn't stop the promise from settling. Use either or both. 4. **Long-running work emits along the way** — `generation:progress`, `chat:statusChanged` — so a UI can narrate without polling anything. 5. **Aborts are not errors.** Cancelling (via `AbortSignal` or `job.cancel()`) settles the promise with an abort rejection but does **not** emit `error` — you asked for it. ### Queryable state Events tell you *when* something changed; state lets you ask *what things are* at any moment. A component that mounts late doesn't need to have been listening — it reads current state and subscribes for changes going forward: | State | Type | Holds | | --- | --- | --- | | `dwm.images.jobs` | `readonly GenerationJob[]` | Every job this client has submitted or resumed, live | | `dwm.recommendations.last` | `RecommendationResult \| undefined` | Most recent `forProducts` result | | `dwm.recommendations.lastSearch` | `SearchResult \| undefined` | Most recent `search` result | | `dwm.estimates.last` | `PriceEstimate \| undefined` | Most recent estimate | | `dwm.chat.status` | `ChatStatus` | `'idle' \| 'ready' \| 'thinking' \| 'failed'` | | `dwm.chat.messages` | `readonly ChatMessage[]` | The active chat's transcript, including action results — oldest-first, paged in by [`loadEarlier()`](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat-dwm-chat-resume-chatid) | | `dwm.chat.hasEarlier` | `boolean` | Whether the active chat has older messages left to load | | `dwm.chat.current` | `{ id, title, ... } \| undefined` | The active chat | | `dwm.activity.log` | `readonly ActivityEntry[]` | Everything the SDK has done this session, in order | State is read-only and updated *before* the corresponding event fires, so reading state inside a handler always sees the new value. ``` // The standard binding pattern: read state, re-read on events. function useDesignWithMeChat() { const [messages, setMessages] = useState(() => [...dwm.chat.messages]); useEffect(() => dwm.on('chat:message', () => setMessages([...dwm.chat.messages])), []); return messages; } ``` ### The activity feed `activity` is the umbrella: one subscription that sees every event, normalized. It exists for logging, analytics, and debug panels — anywhere you want "what is the SDK doing?" without subscribing to each domain. ``` dwm.on('activity', (entry) => { // { at: 1755600000000, type: 'generation:completed', summary: 'Generation job job_x completed (1 image)', detail: {...} } analytics.track('dwm', entry); }); console.table(dwm.activity.log); // the same entries, queryable after the fact ``` Every entry: `at` (epoch ms), `type` (the event name), `summary` (human-readable one-liner), `detail` (the event payload). ### Errors All failures are instances of one class: ``` import { DesignWithMeError, isDesignWithMeError } from '@mxt/designwithme-sdk'; interface DesignWithMeError extends Error { code: string; // stable, machine-readable — switch on this message: string; // human-readable explanation status?: number; // underlying HTTP status, when applicable requestId?: string; // include when contacting support } ``` Codes you can encounter, by category: | Category | Codes | | --- | --- | | Configuration & access | `invalid_api_key`, `origin_not_allowed`, `client_inactive`, `forbidden` | | Bad input | `validation_error`, `unsupported_room_type`, `insufficient_input` | | Result quality | `low_confidence`, `generation_failed` | | Flow | `not_found`, `action_skipped` | | Load & availability | `rate_limited`, `service_unavailable`, `timeout` | | Transport | `network_error`, `unknown_error` | Codes are stable API — new codes may be added, existing ones won't change meaning. Always branch on `code`, never on `message` text. Retry guidance per code lives in the [Integration Guide](/designwithme-ai/docs/0-1-0-designwithme-sdk#integration-error-handling-strategy). ``` dwm.on('error', (error) => { if (error.code === 'rate_limited') scheduleRetry(); else if (error.code === 'network_error') showOfflineBanner(); else log.error(error.code, error.requestId, error.message); }); ``` --- ## Configuration Reference Everything `createDesignWithMe` accepts, plus the settings configured *for* your client account rather than in code. ### Client options ``` import { createDesignWithMe } from '@mxt/designwithme-sdk'; const dwm = createDesignWithMe({ apiKey: 'your-api-key', // required environment: 'development', // optional chat: { // optional onAction: (action) => action.proceed(), }, }); ``` | Option | Type | Default | Description | | --- | --- | --- | --- | | `apiKey` | `string` | — (required) | Your DesignWithMe API key. The client throws immediately if it's missing or blank. | | `environment` | `'development' \| 'cert' \| 'production'` | `'production'` | Which environment to target. Keys are environment-specific, so this must match the environment your key was issued for. Anything other than production must be named explicitly. | | `chat.onAction` | `(action: ActionRequest) => void \| Promise<void>` | auto-proceed | Interception hook for assistant-triggered actions — inspect, then `action.proceed()` or `action.skip()`. Details on the [Interactive Chat](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat-intercepting-actions) page. | | `fetch` | `typeof fetch` | global `fetch` | Custom fetch implementation. For test injection and non-standard runtimes; browsers and Node 18+ never need it. | The options object is read once at creation. To change configuration, create a new client — and note that a new client starts with fresh state (empty `jobs`, no transcript, new session). ### Environments | Environment | Purpose | | --- | --- | | `development` | Integration and testing | | `cert` | Pre-production validation | | `production` | Live traffic (the default) | Each environment is fully isolated: separate API keys, separate catalogs, separate sessions. Nothing crosses over, and there is no way to address one environment's data from another. Which of the three is deployed right now is a rollout status rather than a configuration rule — see [Current rollout status](/designwithme-ai/docs/0-1-0-designwithme-sdk#getting-started-current-rollout-status). The service hosts behind each name are an internal detail; the environment name is the supported knob. ### Account-level settings These are configured on your client account by 3D Cloud, not in code. They shape what the SDK is allowed to do — if a capability seems missing, check here before debugging your integration. | Setting | What it controls | Surfaces as | | --- | --- | --- | | **Origin allowlist** | Which web origins may use your key from a browser | `origin_not_allowed` — though a browser usually reports it as `network_error`, see [Authentication](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication-the-origin-allowlist) | | **Scopes** | Which services the key may call | `forbidden` errors from out-of-scope services | | **Catalog** | The product set behind recommendations and search | The SKUs you get back | | **Assistant integrations** | Which actions the chat assistant may trigger (image generation, recommendations, estimates) | Which `chat:action` types you observe | | **Pricing parameters** | Default cost assumptions for price estimation | Estimate results when you omit `pricePerUnitArea` | | **Rate limits** | Request budget per key | `rate_limited` errors | To change any of these, contact your account representative. > **Warning: Browser keys ride the origin allowlist** > > > The allowlist is what makes an API key in front-end code safe: the key alone is not enough — the request must also come from an origin you registered. Register every origin you serve from, including local development ones, and nothing else. It is enforced by the auth service on your key, not by any network layer you control; 3D Cloud registers origins on request. A server-side key sends no browser origin and so is *not* covered — see [How many keys do I need?](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication-how-many-keys-do-i-need). ### TypeScript The package ships complete declarations; no `@types` package is needed. All request/response/event types are importable: ``` import type { DesignWithMeClient, DesignWithMeEvents, GenerationJob, GenerateImageRequest, RecommendationResult, PriceEstimate, ChatMessage, ChatAction, } from '@mxt/designwithme-sdk'; ``` `DesignWithMeEvents` is the full event map — useful for writing typed wrappers around `dwm.on` in your own framework glue. --- ## Integration Guide Patterns for taking a DesignWithMe integration to production: client structure, framework bindings, surviving page refreshes, error strategy, and a troubleshooting FAQ. ### One client, shared Create one `dwm` instance per application and share it. The client *is* the state — the sign-in, the job list, the transcript, the activity log. Multiple clients mean parallel sign-ins and split state, and there's never a reason for it. ``` // designwithme.ts — the one place the client exists import { createDesignWithMe } from '@mxt/designwithme-sdk'; export const dwm = createDesignWithMe({ apiKey: import.meta.env.VITE_DWM_API_KEY, environment: 'development', }); ``` ### Binding to a framework The SDK is framework-agnostic by design: **read state, re-render on events**. Both halves matter — state gives late-mounting components the current picture; events keep it fresh. Every `dwm.on` returns its own cleanup function, which is the whole integration trick. #### React ``` function useDwmEvent(event: keyof DesignWithMeEvents, select: () => T): T { const [value, setValue] = useState(select); useEffect(() => dwm.on(event, () => setValue(select())), [event]); return value; } // usage const messages = useDwmEvent('chat:message', () => [...dwm.chat.messages]); const jobs = useDwmEvent('generation:progress', () => [...dwm.images.jobs]); ``` #### Lit / web components ``` class ChatPanel extends LitElement { private off?: () => void; connectedCallback() { super.connectedCallback(); this.off = dwm.on('chat:message', () => this.requestUpdate()); } disconnectedCallback() { this.off?.(); super.disconnectedCallback(); } render() { return html`${dwm.chat.messages.map(renderBubble)}`; } } ``` #### Everything at once For stores (Redux, Zustand, signals), the `activity` umbrella event is the single subscription that mirrors all SDK state into yours: ``` dwm.on('activity', () => store.set(snapshotFrom(dwm))); ``` ### Surviving a page refresh The SDK's in-memory state is per-page-load, but the *work* survives — generation jobs keep running, and chats are durable with their transcripts held server-side. Persist two identifiers and re-attach on boot: ``` // persist as they're created dwm.on('generation:submitted', ({ id }) => myState.saveJobId(id)); dwm.on('chat:ready', ({ chatId }) => myState.saveChatId(chatId)); // re-attach on boot for (const jobId of myState.pendingJobIds()) dwm.images.resume(jobId); if (myState.chatId()) await dwm.chat.resume(myState.chatId()); ``` That's the complete refresh story: resumed jobs report through the same handles and events as new ones, and a resumed chat restores its transcript into `dwm.chat.messages` — recent history immediately, older messages on demand via [`loadEarlier()`](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat-dwm-chat-resume-chatid). (For chats you can even skip your own persistence — `dwm.chat.list()` recovers the user's conversations from the service.) ### Error handling strategy Every failure carries a stable `code` ([full list](/designwithme-ai/docs/0-1-0-designwithme-sdk#events-errors)). Branch on it, and let the category pick the response: | Category | Codes | Strategy | | --- | --- | --- | | Configuration | `invalid_api_key`, `origin_not_allowed`, `client_inactive`, `forbidden` | Not retryable. Fix the key, allowlist, or scopes — fail loudly in development, alert in production. | | User-fixable input | `validation_error`, `unsupported_room_type`, `insufficient_input`, `low_confidence` | Not retryable as-is. Turn into guidance: "try a wider photo", "pick a supported room type". | | Transient | `rate_limited`, `service_unavailable`, `network_error` | Retryable with backoff. Don't retry in a tight loop — the SDK has already absorbed transient blips inside long-running work. | | Flow | `not_found` | The referenced thing is gone (or was never yours — the services don't distinguish). Drop the stale id; for a chat, start a fresh one. | Two placements that work together: ``` // 1. A global tap for logging and generic UX — every failure passes through here. dwm.on('error', (error) => log.warn('[dwm]', error.code, error.requestId ?? '', error.message)); // 2. Local handling only where the UX is specific. try { const estimate = await dwm.estimates.create(request); } catch (error) { if (isDesignWithMeError(error) && error.code === 'insufficient_input') return askForBetterPhoto(); throw error; // the global tap already logged it } ``` > **Warning: Avoid wrapping every call** > > > Resist a blanket try/catch around each SDK call with a generic toast. Decide per feature: **degrade** what's supplementary (a recommendations rail that fails just doesn't render) and **surface** what the user explicitly asked for (a failed search or generation deserves a real message). The error `code` is how you tell the difference programmatically. ### Cancellation Everything long-running accepts an `AbortSignal`, and job handles have `cancel()`. Wire component teardown to abort so background work doesn't outlive its UI: ``` const controller = new AbortController(); const job = dwm.images.generate({ ...request, signal: controller.signal }); // on unmount / navigation controller.abort(); ``` Aborts settle promises with an abort rejection and never emit `error` events — cancelling is not a failure. ### Worked example: a minimal room designer The pieces compose into a full experience with very little glue — chat drives the flow, and the other services are both directly callable and assistant-triggerable: ``` await dwm.chat.start({ title: 'Living room refresh' }); // The conversation IS the app: messages and action results render from one list. dwm.on('chat:message', () => renderTranscript(dwm.chat.messages)); dwm.on('chat:action', (action) => { if (action.status !== 'completed') return updateActionChip(action); if (action.type === 'imageGeneration') showScene(action.job.images[0].url); if (action.type === 'recommendations') showRail(action.products); if (action.type === 'estimate') showBudget(action.estimate); }); // Direct calls coexist with the conversation: budgetButton.onclick = () => dwm.estimates.create({ roomType: 'furniture', roomImageUri, unitSystem: 'imperial' }); // The user types; everything above reacts. input.onsubmit = (text) => dwm.chat.send(text); ``` Add the [refresh-survival](/designwithme-ai/docs/0-1-0-designwithme-sdk#integration-surviving-a-page-refresh) block and a `chat:statusChanged` banner, and this is production-shaped. ### Performance notes - **Calls are independent and safely concurrent** — parallel estimates, several generation jobs, recommendations during a generation: all fine. The client coordinates authentication internally; there's no benefit to serializing SDK calls. - **Don't refetch recommendations while they're on screen** — results can reorder between calls. Fetch per context change, render from `dwm.recommendations.last` ([why](/designwithme-ai/docs/0-1-0-designwithme-sdk#product-recommendations-events-state)). - **Generated image URLs are short-lived (~1 hour).** Render immediately, and use `job.refreshImages()` for anything re-displayed later ([details](/designwithme-ai/docs/0-1-0-designwithme-sdk#image-generation-image-freshness-job-refreshimages)). - **`thinkingLevel` is your latency dial** for generation quality; start at the default and raise it only where quality demands it. ### Troubleshooting FAQ **Every call fails with `invalid_api_key`.** The key, or the key/environment pairing, is wrong — a development key only works with `environment: 'development'`. Keys never work cross-environment. **Works locally, fails when deployed — usually as `network_error`, sometimes as `origin_not_allowed`.** The deployed origin isn't on your key's allowlist. Origins match exactly — scheme, host, and port. Because the refusal is a `403` on a cross-origin request, the browser blocks the response and your code sees `network_error` with a CORS message in the console; only a non-browser caller sees the `origin_not_allowed` code itself. Have the new origin registered before you go looking for a connectivity problem — see [the origin allowlist](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication-the-origin-allowlist). **I have one key — do I need a second one for my backend?** Only if you call the services from your backend as well. A browser key is constrained by the origin allowlist; a server key isn't origin-bound and is a real secret, so the two want separate lifecycles. One key per environment is otherwise the whole story — see [How many keys do I need?](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication-how-many-keys-do-i-need). **One service fails with `forbidden` while the others work.** The key isn't scoped for that service. Scopes are per-key, server-side — ask your account representative. **The chat assistant never triggers actions.** Assistant integrations are enabled per account ([settings](/designwithme-ai/docs/0-1-0-designwithme-sdk#configuration-account-level-settings)). If chat answers but never acts, the integration isn't enabled for your client. **`resume(jobId)` fails with `not_found`.** Job records are kept ~30 days; older ids are gone. Drop stale ids when you see this code. **A generated image renders at first, then breaks later.** You persisted the image URL — it expires after about an hour. Persist the job id and call `refreshImages()`, or download the image to your own storage. **Two parts of my UI show different chat states.** Almost always two client instances. Share one `dwm` (see [One client, shared](/designwithme-ai/docs/0-1-0-designwithme-sdk#integration-one-client-shared)) — the client is the single source of truth. --- ## Image Generation Generate photorealistic room scenes: supply a photo of the room and the products to place in it, and get back one or more generated images. Generation takes seconds, so the SDK gives you a **job handle** — submit, watch progress, await the result, and resume tracking after a page refresh. ``` const job = dwm.images.generate({ roomImageUri: 'https://example.com/uploads/living-room.jpg', products: [ { id: 'SOFA-9412', quantity: 1 }, { id: 'CHAIR-2210', quantity: 2 }, { imageUri: 'https://example.com/uploads/heirloom-lamp.jpg', name: 'Heirloom lamp' }, ], prompt: 'Warm evening lighting, keep the rug visible', }); const images = await job.result(); render(images[0].url); ``` ### `dwm.images.generate(request)` Returns a `GenerationJob` handle **synchronously** — submission happens in the background, and the handle reports every step. You can attach listeners or `await job.result()` immediately; there is no gap to miss. #### Request | Field | Type | Required | Description | | --- | --- | --- | --- | | `roomImageUri` | `string` | yes | URL of the room photo. Always the *original* room photo — see [Iterating on a scene](/designwithme-ai/docs/0-1-0-designwithme-sdk#image-generation-iterating-on-a-scene). | | `products` | `ProductRef[]` | yes | The products to place. At least one. | | `prompt` | `string` | no | Free-text styling and layout direction ("warm evening lighting", "rug stays visible"). | | `aspectRatio` | `string` | no | `"W:H"`, e.g. `"3:2"`. Omit to match the room photo. | | `thinkingLevel` | `'minimal' \| 'low' \| 'medium' \| 'high'` | no | Quality/latency trade-off. Higher levels compose more carefully and take longer. Default `'minimal'`. | | `projectId` | `string` | no | Groups the job and its output under one of your project identifiers. | | `signal` | `AbortSignal` | no | Aborting cancels submission or stops tracking — same effect as [`job.cancel()`](/designwithme-ai/docs/0-1-0-designwithme-sdk#image-generation-cancelling). | #### Products Two kinds of product reference, distinguished by which field you provide: ``` type ProductRef = | { id: string; name?: string; quantity?: number } // a product from your catalog | { imageUri: string; name?: string; quantity?: number }; // a customer-supplied product photo ``` - **Catalog products** are referenced by `id`; the service already knows what they look like. `name` helps composition quality — include it when you have it. - **Custom products** are anything with a photo — a customer's own furniture, an heirloom. Provide `imageUri` and a `name`. - **`quantity`** places multiples of the same product (default 1). Two accent chairs is `{ id: 'CHAIR-2210', quantity: 2 }`. > **Tip: Keep product counts practical** > > > Composition quality degrades as the product list grows — roughly ten products is a practical ceiling for one scene. Placing a whole room? Prioritize the anchor pieces and let decor strategies fill in around them. ### The job handle ``` interface GenerationJob { readonly id: string | undefined; // set once submitted (see generation:submitted) readonly status: GenerationStatus; // 'submitting' | 'queued' | 'generating' | 'complete' | 'failed' readonly images: GeneratedImage[]; // populated when complete readonly error?: DesignWithMeError; // populated when failed readonly createdAt: number; // epoch ms result(): Promise; // resolves on complete, rejects on failure refreshImages(): Promise; cancel(): void; on(event: 'progress' | 'completed' | 'failed', handler): () => void; } interface GeneratedImage { url: string; // display-ready image URL index: number; // variant index, 0-based name?: string; // stable identifier for the image } ``` #### Status lifecycle ``` submitting → queued → generating → complete ↘ ↘ failed ``` Statuses only move forward. `complete` and `failed` are terminal. The SDK tracks the job for up to five minutes of activity; a job that never reaches a terminal state in that window fails with a timeout error. Transient hiccups while tracking are absorbed silently — you only hear about real outcomes. #### Watching progress Job handles emit their own scoped events, and every job also reports through the client-level `generation:*` events (see [Events & State](/designwithme-ai/docs/0-1-0-designwithme-sdk#events-image-generation)) — handle events for the component that made the job, client events for anything else that cares: ``` job.on('progress', ({ status }) => setLabel(status === 'queued' ? 'Waiting…' : 'Generating…')); job.on('completed', ({ images }) => render(images)); job.on('failed', ({ error }) => showError(error)); ``` #### Cancelling `job.cancel()` (or aborting the request's `signal`) stops the SDK from tracking the job and settles `result()` with an abort rejection. Cancelling is client-side — a generation already running may still finish on the service, but nothing will be reported for it. Aborts do not emit `error` events. ### Resuming after a refresh — `dwm.images.resume(jobId)` Generation outlives page loads. Persist `job.id` wherever you keep your own state (it's available from `generation:submitted` onward), and after a reload, re-attach: ``` // before: persist the id once the job is accepted dwm.on('generation:submitted', ({ id }) => saveToMyState(id)); // after a refresh: pick the job back up — same handle API, same events const job = dwm.images.resume(savedJobId); const images = await job.result(); ``` `resume` returns immediately with a tracking handle; if the job finished while you were away, it reports `complete` (or `failed`) on the first check. Unknown or expired job ids fail with `not_found`. Job records are kept for about 30 days. ### Image freshness — `job.refreshImages()` The `url` on a generated image is **display-ready but short-lived** (about an hour). Rendering it immediately is always fine. If you re-display a result much later — a gallery revisited the next day — refresh first: ``` const images = await job.refreshImages(); // same images, fresh URLs ``` For long-term storage, download the image or persist `job.id` and refresh on demand — don't persist the `url` itself. ### Live state — `dwm.images.jobs` Every job this client has created or resumed, in creation order, live: ``` const active = dwm.images.jobs.filter((j) => j.status !== 'complete' && j.status !== 'failed'); badge.count = active.length; ``` Multiple jobs run concurrently without any coordination on your part — each handle tracks independently, and client-level events carry the job so handlers can tell them apart. ### Iterating on a scene To refine a generated scene ("make it warmer", "swap the rug"), submit a **new job with the original room photo** and a prompt that restates the full direction so far — don't feed a generated image back in as the room photo. Re-generating from a generated image compounds visual loss; restating the accumulated prompt against the original photo is the intended pattern, and it's exactly what the [chat assistant](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat) does when a conversation iterates on a room. ``` const prompts: string[] = []; function refine(newDirection: string) { prompts.push(newDirection); return dwm.images.generate({ roomImageUri: originalPhoto, products, prompt: prompts.join('\n') }); } ``` ### Errors | Code | Meaning | | --- | --- | | `validation_error` | Bad request — missing room photo, empty products, malformed `aspectRatio` | | `not_found` | `resume` was given an unknown or expired job id | | `forbidden` | Your key isn't scoped for image generation | | `rate_limited` | Too many jobs — back off and retry | | `service_unavailable` | Generation is down; retry later | | `network_error` | Couldn't reach the service | Failures reject `job.result()`, set `job.error`, and emit `generation:failed` plus the global `error` event — the [one model](/designwithme-ai/docs/0-1-0-designwithme-sdk#events-promises-and-events-always-agree) everywhere. --- ## Product Recommendations Two complementary calls over **your product catalog**: - **`forProducts`** — given products (a cart, a scene), return ranked recommendations under a chosen strategy. - **`search`** — free-text style search with optional attribute filters. Both resolve from your own catalog — your API key scopes them; there's no catalog id to pass. Both return the same simple shape: ranked products with a **match score where higher is always better** (`1` = perfect match, `0` = none), regardless of which call produced it. ``` const { products } = await dwm.recommendations.forProducts({ skus: ['SOFA-9412', 'RUG-3307'], strategy: 'complementary', limit: 5, }); ``` ### `dwm.recommendations.forProducts(request)` #### Request | Field | Type | Required | Description | | --- | --- | --- | --- | | `skus` | `string[]` | yes | The input products. An empty array short-circuits to an empty result — no call, no error. | | `strategy` | `Strategy` | no | How to recommend — see below. Default `'similar'`. | | `limit` | `number` | no | Maximum results. Default 10. | | `threshold` | `number` | no | Drop results scoring below this (0–1). Omit for the service default. | | `signal` | `AbortSignal` | no | Abort the call. | #### Strategies | Strategy | Returns | | --- | --- | | `'similar'` | Substitutes and close variants of the inputs | | `'complementary'` | Complete-the-room pairings — what goes *with* the inputs | | `'decor'` | Decorative accents for the inputs | | `'priceAlternative'` | Cheaper or comparable-price alternatives, per input product | #### Result ``` interface RecommendationResult { products: Array<{ sku: string; // recommended product, from your catalog score: number; // 0–1 match, higher is better sourceSku?: string; // which input this alternates for — priceAlternative only }>; strategy: Strategy; } ``` Results are ranked best-first, never include the input SKUs, and may be shorter than `limit` (or empty) when little clears the bar — an empty list is a valid result, not an error. ``` const { products } = await dwm.recommendations.forProducts({ skus: cartSkus, strategy: 'priceAlternative' }); for (const alt of products) { console.log(`${alt.sourceSku} → try ${alt.sku} (${(alt.score * 100).toFixed(0)}%)`); } ``` ### `dwm.recommendations.search(request)` Style search in plain language — "mid-century walnut credenza with brass hardware" — with optional attribute filters. #### Request | Field | Type | Required | Description | | --- | --- | --- | --- | | `query` | `string` | yes | The style description. Blank input short-circuits to an empty result. | | `filters` | `SearchFilters` | no | Attribute constraints — see below. | | `limit` | `number` | no | Maximum results. Default 10, capped at 50. | | `signal` | `AbortSignal` | no | Abort the call. | #### Filters ``` interface SearchFilters { color?: string[]; brand?: string[]; manufacturer?: string[]; finish?: string[]; family?: string[]; style?: string[]; } ``` Within one field, values are **OR** (`color: ['walnut', 'brown']` — either). Across fields, **AND** (that color *and* that style). Matching is case-insensitive. There is no category filter — fold the category into the query text instead: searching "retro" within sofas works best as `query: 'retro sofas'`. #### Result ``` interface SearchResult { products: Array<{ sku: string; score: number }>; // ranked best-first, higher is better } ``` Search has no pagination — it returns one ranked set, capped at 50. An empty `products` array means nothing in your catalog matched; show a no-results state, not an error. ``` const { products } = await dwm.recommendations.search({ query: 'mid-century walnut credenza with brass hardware', filters: { style: ['mid-century'] }, limit: 20, }); ``` ### Results are SKUs — hydrate for display Both calls return SKUs and scores, not display data. Look prices, names, and images up in your own product system, and **preserve the SDK's order** when you render — the ranking is the product of the call: ``` const { products } = await dwm.recommendations.forProducts({ skus, strategy: 'similar' }); const details = await myCatalog.bySkus(products.map((p) => p.sku)); const ordered = products.map((p) => details.get(p.sku)).filter(Boolean); ``` ### Events & state | | | | --- | --- | | Events | `recommendations:completed` / `recommendations:failed`, `search:completed` / `search:failed` | | State | `dwm.recommendations.last`, `dwm.recommendations.lastSearch` | > **Tip: Keep recommendations stable while they are on screen** > > > Re-requesting with the same inputs can reorder results as the catalog evolves. Fetch once per context (a scene, a cart state) and render from `dwm.recommendations.last` — don't refetch on window focus or reconnect while the user is looking at them. ### Errors | Code | Meaning | | --- | --- | | `validation_error` | Bad request — unknown strategy or filter field, out-of-range `threshold` | | `forbidden` | Your key isn't scoped for recommendations | | `rate_limited` | Too many requests — back off and retry | | `service_unavailable` | Service is down; retry later | | `network_error` | Couldn't reach the service | A common production pattern: recommendations degrade gracefully (a failed rail just doesn't render), while search failures surface to the user — search is their primary intent. Both are your call; the [Integration Guide](/designwithme-ai/docs/0-1-0-designwithme-sdk#integration-error-handling-strategy) discusses it. --- ## Price Estimation Estimate a room's dimensions and project cost from a single photo. One call in, one estimate out: floor and wall areas, a cost range with a per-component breakdown, and a confidence score on every figure. Stateless — no job to watch, no session to manage. ``` const estimate = await dwm.estimates.create({ roomType: 'kitchen', roomImageUri: 'https://example.com/uploads/kitchen.jpg', unitSystem: 'imperial', }); showRange(estimate.cost.low, estimate.cost.high, estimate.cost.currency); ``` ### `dwm.estimates.create(request)` #### Request | Field | Type | Required | Description | | --- | --- | --- | --- | | `roomType` | `'kitchen' \| 'bath' \| 'furniture' \| 'decking'` | yes | What's being priced. Anything else fails with `unsupported_room_type`. | | `roomImageUri` | `string` | yes | URL of the room photo. | | `unitSystem` | `'imperial' \| 'metric'` | yes | Imperial = square feet + USD. Metric = square meters + your chosen currency. | | `pricePerUnitArea` | `number` | no | Your cost assumption **in the requested unit system** (per sq ft or per sq m). Omit to use the pricing parameters configured for your account. | | `currency` | `string` | no | ISO 4217, **metric only** — imperial is always USD. | | `notes` | `string` | no | Free-text context ("galley kitchen, standard ceiling height"). Context raises confidence. | | `signal` | `AbortSignal` | no | Abort the call. | > **Warning: Price per unit area follows the unit system** > > > The same number means very different budgets per square foot vs per square meter (1 sq m ≈ 10.76 sq ft). If you let users switch unit systems, convert your price assumption when you switch — don't reuse the number. #### Result ``` interface PriceEstimate { roomType: RoomType; unitSystem: UnitSystem; floorArea: AreaEstimate; // { value, unit: 'sqft' | 'sqm', confidence } wallArea: AreaEstimate; cost: CostEstimate; // { low, high, currency, confidence } breakdown: BreakdownItem[]; // [{ component, low, high }] } ``` - **Ranges, not points.** `cost.low`–`cost.high` is the honest answer to "what will this cost?" — present it as a range. - **Confidence is 0–1** on `floorArea`, `wallArea`, and `cost`. Low confidence means the photo left the model guessing — a wider shot or `notes` context improves it. - **Breakdown components vary by room type** (cabinets/countertops/appliances/labor for a kitchen; fixtures/tile/vanity for a bath; …). Render whatever comes back — don't hard-code the component list. Breakdown lines carry no per-line confidence; the range *is* the uncertainty. ``` for (const line of estimate.breakdown) { row(line.component, formatRange(line.low, line.high, estimate.cost.currency)); } ``` ### Both unit systems at once Each call is independent, so showing imperial and metric side by side is just two concurrent calls: ``` const [imperial, metric] = await Promise.all([ dwm.estimates.create({ roomType, roomImageUri, unitSystem: 'imperial', pricePerUnitArea: 45 }), dwm.estimates.create({ roomType, roomImageUri, unitSystem: 'metric', pricePerUnitArea: 45 * 10.7639, currency: 'EUR' }), ]); ``` ### Events & state | | | | --- | --- | | Events | `estimate:completed` / `estimate:failed` | | State | `dwm.estimates.last` | The chat assistant can also produce estimates mid-conversation — those flow through [`chat:action`](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat-assistant-actions) and land in the transcript, and the underlying estimate still updates `dwm.estimates.last`. ### Errors | Code | Meaning | What to do | | --- | --- | --- | | `validation_error` | Missing/invalid fields | Fix the request | | `unsupported_room_type` | `roomType` isn't supported | Offer only the four supported types | | `insufficient_input` | The photo couldn't be used — unreachable, or not enough visible room to measure | Ask the user for a wider, clearer photo | | `low_confidence` | The model couldn't reach a publishable estimate from this photo | Same remedy — better photo or added `notes`; there is no partial result | | `forbidden` | Your key isn't scoped for estimation | Have the key's scopes checked | | `rate_limited` | Too many requests | Back off and retry | | `service_unavailable` | Service is down | Retry later | `insufficient_input` and `low_confidence` are **user-fixable** — treat them as prompts to improve the input, not as system failures. --- ## Interactive Chat A conversational design assistant that **acts**, not just answers: mid-conversation it can generate an image of the user's room or find products for it. The SDK runs the whole thing — the conversation transport, the persistent chat history, and the execution of every assistant-triggered action — and reports each step through [the one event model](/designwithme-ai/docs/0-1-0-designwithme-sdk#events). > **Note: Rollout status** > > > `dwm.chat` is implemented and reachable in the `development` environment — chats, transcripts, attachments, the streamed reply, and the assistant's action loop all run there today. What is still outstanding is the service side: the payload shape for assistant-triggered actions is not finalized, and the conversation endpoints are not yet deployed beyond development. This is a deployment status, not a caveat on the API described below. ``` const chat = await dwm.chat.start({ title: 'Living room refresh' }); dwm.on('chat:message', (message) => renderBubble(message)); dwm.on('chat:action', (action) => { if (action.type === 'imageGeneration' && action.status === 'completed') { renderInChat(action.job.images[0].url); } }); dwm.chat.send('Show me this room with a mid-century refresh'); ``` That's a complete chat integration with image generation included. No transport code, no message protocol, no orchestration. ### Chats are persistent A chat is not a fragile session — it's a durable conversation owned by your user. There is nothing to keep alive, nothing expires, and nothing needs ending: a chat started last week resumes exactly where it left off, history included. #### `dwm.chat.start(options?)` Creates a new chat and makes it the active one. Resolves when the assistant is ready for input. | Option | Type | Description | | --- | --- | --- | | `title` | `string` | Optional display title for the chat list. No default is generated. | #### `dwm.chat.resume(chatId)` Makes an existing chat the active one and loads its transcript — the assistant remembers everything. Persist `dwm.chat.current.id` in your own state and resume after a page load: ``` const existing = loadMySavedChatId(); if (existing) await dwm.chat.resume(existing); else await dwm.chat.start(); render(dwm.chat.messages); // most recent history, either way ``` `resume` loads the **most recent** page of history, not the entire conversation — a chat with a thousand messages resolves as fast as a new one. Older messages come in on demand: ``` // "Scroll up to load more" if (dwm.chat.hasEarlier) await dwm.chat.loadEarlier(); ``` `loadEarlier()` prepends the previous page to `dwm.chat.messages` and resolves once it's in. `dwm.chat.hasEarlier` is `true` while there is more history to fetch and `false` once the start of the conversation is loaded — so the snippet above is the whole of "load more". Both are safe on a chat you just started. An unknown chat id fails with `not_found` — which is also what you see for a chat that isn't yours; the service doesn't distinguish. #### `dwm.chat.list(options?)` Your user's chats, most recently active first — the data behind a "Conversations" screen: ``` const { chats, nextCursor } = await dwm.chat.list({ limit: 20 }); // chats: [{ id, title, messageCount, createdAt, updatedAt }] const more = await dwm.chat.list({ limit: 20, cursor: nextCursor }); ``` `nextCursor` is absent on the last page; pass it back as `cursor` to keep walking. `limit` defaults to 50 and tops out at 200 — a larger value is clamped, not rejected. One client serves one active conversation at a time — `list` is for navigation, `resume` switches. Two details worth knowing if you page deeply: - **Ordering is by last activity.** Sending a message moves that chat to the front, so a chat touched while the user is paging may appear twice or be skipped. Key your list rendering by `id` and the duplicate collapses. - **`messageCount` counts stored messages, not rendered ones.** The service records an assistant turn even when the reply came back empty, so the count can run slightly ahead of what's in the transcript. Treat it as an activity signal, not a total to paginate against. ### Talking — `dwm.chat.send(text, options?)` ``` dwm.chat.send('What sofa would fit in this room?'); ``` The user's message lands in the transcript immediately, and the assistant's reply **streams**: the SDK appends text to a pending assistant message as it arrives, firing `chat:delta` with each fragment and `chat:message` once the reply is complete. Render from state and a typing bubble falls out for free: ``` dwm.on('chat:delta', ({ text }) => pendingBubble.textContent = text); // the growing reply dwm.on('chat:message', () => renderTranscript(dwm.chat.messages)); // the finished message ``` | Option | Type | Description | | --- | --- | --- | | `attachments` | `ChatAttachment[]` | Files to send with the message — see [Attachments](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat-attachments). At most 3. | `text` may be omitted when `attachments` is present: sending a room photo with no caption is a normal request. Sending neither fails with `validation_error`. #### Delivery is resilient by design The service records your message before the assistant ever sees it, so a dropped connection can't lose it. There is no stream to resume — instead the SDK asks the service for whatever landed after the last message it saw, and delivers the finished reply as the same `chat:message` you'd have got from the stream. Your message is never re-sent, and you don't poll, retry, or check anything. Two ways a reply can come back less than whole, both visible on the message itself: - **`truncated`** — the reply is real but cut short: the stream failed partway, or the model hit its output ceiling. The partial text is persisted and stays in the transcript. - **`blocked`** — the request was refused by safety screening, so `text` is empty and nothing was ever generated. Distinguishing this from an ordinary short answer is why the flag exists. Neither is an error: the chat stays healthy and the user can simply say something else. ### Attachments The assistant can *see* — send a room photo alongside the text: ``` // Best practice: upload on file-pick, so send is instant. const photo = dwm.chat.upload(fileInput.files[0]); // returns an attachment handle immediately dwm.chat.send('What would suit this room?', { attachments: [photo] }); ``` `upload(file)` registers the file and transfers it in the background, returning a handle synchronously — call it the moment the user picks a file and by send time it's usually done. `send` waits for any transfer still in flight, so you never have to sequence the two yourself: ``` interface ChatUpload { fileId: string | undefined; // set once the service has registered the file name: string | undefined; contentType: string; size: number; state: 'uploading' | 'ready' | 'failed'; error: DesignWithMeError | undefined; ready(): Promise; // resolves when the transfer lands } ``` You only need `ready()` if you want to show upload progress or catch a rejection at file-pick time; passing the handle to `send` is enough otherwise. | Limit | Value | | --- | --- | | Attachments per message | 3 | | File types | An allowlist configured for your account (images, PDF, CSV, JSON, plain text) | | File size | A per-type ceiling, also configured for your account | A file outside the allowlist or over its ceiling is refused **before any bytes are transferred** — the limits are checked when the file is registered, not after a slow upload, so the rejection lands on `upload.ready()` (and on the `send` that carries it) rather than wasting the transfer. Exceeding three attachments, or sending a file whose transfer never completed, fails the `send` with `validation_error`. Messages carry their attachments in the transcript: ``` interface ChatAttachment { fileId: string; name?: string; contentType: string; size?: number; // absent on attachments read back from history } const url = await dwm.chat.fileUrl(attachment.fileId); // fresh display URL ``` **Always fetch the display URL at render time** — `fileUrl` returns a short-lived URL (about an hour) while chat history lives forever; never persist or cache the URL itself. `contentType` is the type the file was uploaded as, so a CSV attachment can be rendered as a table rather than a download link. Files are reusable across messages and chats, including images the assistant itself generated in earlier turns: ``` const { files, nextCursor } = await dwm.chat.uploads.list({ limit: 20 }); // files: ChatAttachment + { state: 'pending' | 'ready', origin: 'upload' | 'tool', createdAt } const reusable = files.filter((file) => file.state === 'ready'); dwm.chat.send('Use this one again', { attachments: [reusable[0]] }); ``` `uploads.list()` pages newest-first with the same `limit`/`cursor` contract as `chat.list()`. Two fields are worth filtering on: `origin` distinguishes the user's own uploads from images the assistant generated, and `state` is `'pending'` for a file whose bytes never arrived — attaching one fails the send, so skip them when offering files for reuse. ### The transcript — `dwm.chat.messages` ``` interface ChatMessage { id: string; role: 'user' | 'assistant'; text: string; attachments?: ChatAttachment[]; // files sent with, or produced by, this message actions?: ChatAction[]; // results of assistant-triggered work, attached in place pending?: true; // an assistant reply still streaming in truncated?: true; // the reply was cut short — see Delivery blocked?: true; // the request was refused by safety screening; text is empty createdAt: number; // epoch ms } ``` The transcript is live and ordered oldest-first — including everything `resume` and `loadEarlier` bring in, which the SDK normalizes for you. Render from state, refresh on `chat:message`: ``` useEffect(() => dwm.on('chat:message', () => setMessages([...dwm.chat.messages])), []); ``` ### Assistant actions When the conversation calls for it — "show me", "what would go with this", "what would that cost" — the assistant requests one of the other services. **The SDK executes the request itself**: it runs the service call (an image generation is a real job, polled to completion), reports the result back to the assistant, and the assistant continues the conversation with the outcome in hand. All of it is narrated via `chat:action` events while each action's result attaches to the transcript message that produced it. A single turn can go around this loop **up to three times** — the assistant can act on what an action returned and act again. Several actions requested together run in parallel, and each reports as it finishes rather than waiting on the slowest. Actions failing never strands the conversation: a failed action is reported to the assistant as a failure, so its next reply can acknowledge and adapt rather than leave a silent gap. The same is true of an action type this SDK version doesn't implement — the assistant is told, and carries on. Occasionally a turn ends *after* its actions without a further assistant reply: the user sent a new message while the work was running, or the turn used all three of its rounds. The action results still attach to the transcript and the chat returns to `ready` — nothing is lost, there just isn't a closing sentence. ``` interface ChatAction { id: string; type: 'imageGeneration' | 'recommendations'; status: 'requested' | 'running' | 'completed' | 'failed'; prompt?: string; // imageGeneration — what the assistant asked for job?: GenerationJob; // imageGeneration — the live job handle query?: string; // recommendations — what the assistant searched products?: Array<{ sku: string; score: number }>; // recommendations — the results error?: DesignWithMeError; // when failed } ``` `chat:action` fires on every status change of the same action object: ``` requested → running → completed ↘ failed ``` ``` dwm.on('chat:action', (action) => { switch (action.status) { case 'running': showThinking(action.type); break; case 'completed': renderResult(action); break; case 'failed': renderApology(action.error); break; } }); ``` Three things worth knowing: - **Actions flow through the regular services.** An assistant-triggered generation is a real `GenerationJob`: it appears in `dwm.images.jobs`, fires `generation:*` events, and `action.job` is the same handle you'd get from `dwm.images.generate` — `refreshImages()`, `result()`, all of it. Recommendations update `dwm.recommendations.last` the same way. - **Generated images become attachments.** An image the assistant produces joins the user's files, so it turns up in `dwm.chat.uploads.list()` (with `origin: 'tool'`) and can be sent back in a later message — "now try that one in grey" needs no re-upload. - **Which action types you see is an account capability.** Assistant integrations are enabled per client (see [account-level settings](/designwithme-ai/docs/0-1-0-designwithme-sdk#configuration-account-level-settings)). A type that isn't enabled simply never occurs. - **The assistant cannot price a room.** There are two action types, not three. Price estimation is a direct API (`dwm.estimates`) that your own UI calls — the model is never offered it as a tool, so no `chat:action` will ever carry one. ### Intercepting actions By default the SDK proceeds with every assistant-triggered action. To inspect first — confirm with the user, skip under conditions, attach analytics — provide `chat.onAction` at client creation: ``` const dwm = createDesignWithMe({ apiKey, environment: 'development', chat: { onAction: async (action) => { if (action.type === 'imageGeneration' && !userAllowsGeneration()) { action.skip('User disabled image generation'); return; } action.proceed(); }, }, }); ``` - The hook fires at `requested`, before the SDK acts. It may be async — the action waits. - Call **`action.proceed()`** to continue or **`action.skip(reason?)`** to decline. Returning without calling either proceeds (the safe default). - A skipped action reports `failed` with the code `action_skipped` — and, like any failed action, the decline is passed back to the assistant, so the conversation continues gracefully ("I wasn't able to generate that image…") instead of stalling. - The hook observes and gates; it doesn't rewrite the assistant's request. ### Conversation status Whatever the transport does under the hood — streamed replies, follow-up polling, retries — your visibility is one status value and its events: | | | | --- | --- | | State | `dwm.chat.status` — `'idle' \| 'ready' \| 'thinking' \| 'failed'` | | Transitions | `chat:statusChanged` — `{ current, previous, reason? }` | | Sugar | `chat:ready`, `chat:failed` | | The active chat | `dwm.chat.current` — `{ id, title, createdAt, updatedAt } \| undefined` | `thinking` is a turn in flight — the natural driver for a typing indicator. `failed` means the active chat can't currently converse (the `reason` says why); the history you already have stays readable, and `resume` or `start` recovers. A *rejected* message is not a failed chat: an empty message, too many attachments, or a file that never finished uploading throws `validation_error` and leaves the status at `ready`, because the very next message can succeed. > **Warning: Resuming mid-turn** > > > If the page reloads while the assistant is waiting on an action, `resume` restores the transcript but does not pick the turn back up — the conversation is readable and you can send a new message, but that interrupted turn stays unfinished. Completing it on resume isn't built yet. ``` dwm.on('chat:statusChanged', ({ current }) => { typingIndicator.hidden = current !== 'thinking'; }); ``` ### Errors | Code | Meaning | | --- | --- | | `not_found` | Unknown chat or file id — including one that belongs to someone else; the service doesn't distinguish | | `forbidden` | Your key isn't scoped for chat | | `validation_error` | A malformed request — an empty message with no attachments, more than 3 attachments, a file type or size outside your account's limits, a file whose transfer never completed, a bad pagination cursor | | `unauthorized` | The session couldn't be established or renewed — see [Authentication](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication) | | `action_skipped` | Your `onAction` hook declined the action (surfaces on the action, not as a thrown error) | | `rate_limited` | Slow down | | `service_unavailable` | Chat is down; the SDK retries transient failures before reporting this | | `turn_failed` | The reply failed after the stream opened — the chat stays resumable | | `network_error` | Couldn't reach the service | Failures during a conversation don't lose it: a failed action reports on the action, a failed turn reports and leaves the chat resumable, and the transcript you've loaded stays readable throughout. A reply that arrived incomplete is not an error at all — see [`truncated` and `blocked`](/designwithme-ai/docs/0-1-0-designwithme-sdk#chat-delivery-is-resilient-by-design). --- ## Example Pages Working, end-to-end example pages ship with every SDK release — one per service, hosted alongside the SDK and pinned to the same version as these docs. Each page is a complete SDK integration, exactly as documented here, and every page's source is clean, commented, and copy-paste-ready. Each page also shows the SDK's live **activity feed**, so you can watch every event fire as the flow runs. [**Open the example pages ↗**](https://sdk-a.designwithme.ai/examples/) | Example | What it demonstrates | | --- | --- | | [Image Generation](https://sdk-a.designwithme.ai/examples/image-generation.html) | `dwm.images.generate` → a live job handle: progress events while it works, the generated scene when it's done. | | [Product Recommendations](https://sdk-a.designwithme.ai/examples/product-recommendations.html) | `dwm.recommendations.forProducts` by strategy, plus free-text style search — one score scale for both. | | [Price Estimation](https://sdk-a.designwithme.ai/examples/price-estimation.html) | `dwm.estimates.create` — imperial and metric side by side, ranges, breakdowns, and confidence. | | [Interactive Chat](https://sdk-a.designwithme.ai/examples/chat.html) | `dwm.chat` — a streamed reply, and the assistant triggering a generation or a product search mid-conversation. | ### The two URL parameters Every example page reads two query parameters. Both are page behaviour, not SDK features — nothing here has an equivalent in your own integration. | Parameter | Default | Effect | | --- | --- | --- | | `?mock=1` | off | Answer every API call from an in-browser mock instead of the real services. No API key needed. | | `?autorun=1` | off | Run the page's flow on load instead of waiting for you to press the button. | They combine: `?mock=1&autorun=1` opens a page that has already run itself, which is the fastest way to see a finished result. Drop the parameter (or set `?mock=0`) to go back to the live services. #### `?mock=1` — what it actually does Mock mode replaces the browser's `fetch` for DesignWithMe URLs only. Requests to `/api/v1/**` are answered by a mock that ships inside the page; every other request passes through, and everything else on the page — including the SDK itself — is completely unchanged and unaware that it is not talking to a server. ``` ?mock=0 (default) page → SDK → fetch → glados → the real AI services ?mock=1 page → SDK → fetch → in-browser mock (never leaves the tab) ``` Concretely, with `?mock=1`: - **No API key is needed and no network request leaves the browser.** The mock answers `POST /api/v1/auth/token` with a fabricated session, so the SDK's sign-in succeeds and every later call is accepted. - **The wire contracts are the real ones.** The mock returns the same JSON shapes the services return, including chat's SSE streaming and the multi-request tool loop, so the SDK's parsing, event ordering, job polling, and state updates are all genuinely exercised. - **Responses are canned and instant-ish.** Every call takes a fixed ~350 ms; an image-generation job "completes" after ~2.5 s. Nothing is generated, scored, or priced — the numbers, SKUs, and images are fixtures. - **The forms fill themselves in.** The pages ship no real room photos or catalog ids, so in mock mode each required field adopts its own placeholder (an invented SKU, a `storage.example.com` URL). That is what makes `?autorun=1` work without a key. So mock mode is the right way to read the code, watch the event sequence, and see the shape of each result. It cannot tell you anything about real latency, real image quality, your own catalog, or whether your key and origin are set up correctly — for that, run live. #### `?autorun=1` — what it actually does `?autorun=1` presses the page's own primary button for you once the page has loaded, using whatever is in the form. It exists so a page can be linked as a finished demo, and so our screenshot and smoke tests can drive it. In mock mode the form is pre-filled, so `?mock=1&autorun=1` runs unattended. In live mode the fields start empty and required, so `?autorun=1` on its own does nothing until you have filled the form in and supplied a key. ### Running live Live is the default: the pages call the real DesignWithMe services, so each one needs an API key in its **Connection** panel. Two things have to line up before a live run works: - **The key must match the environment** selected in the same panel. Keys are issued per environment and never work across one. - **The page's origin must be registered on your key.** The hosted example pages are served from an origin we have registered; a copy you host yourself, or one you run on `localhost`, is a different origin and will be refused until it is registered too. See [Authentication](/designwithme-ai/docs/0-1-0-designwithme-sdk#authentication-the-origin-allowlist) — including why that refusal reaches your code as `network_error` rather than `origin_not_allowed`. ### Running the examples locally The examples live in the docs app (`apps/sdk-docs/examples/` in the SDK repository) and build into the hosted site: ``` pnpm --filter @mxt/designwithme-sdk-docs run dev # → http://localhost:5175/examples/ ``` The dev server hot-reloads the example pages as you edit them. Local origins are not on the API key's allowlist, so add `?mock=1` when running locally unless your own origin has been registered.