Headless Integration
Table of Contents
Headless integration means you own the customer experience—web, app, or other clients—and Stella supplies questionnaire definitions, analysis, and matching variant IDs over HTTPS. You keep product presentation, accounts, cart, and checkout. No Stella JavaScript, iframe, or web component is required.
This is the API for that model.
The embeddable shop components (widget, chat, PDP button, popup) are documented separately in Getting Started and Documentation. Use those when Stella UI runs inside your storefront. Use this page when you render the quiz and results yourself.
If you already call POST /api/analyses for a custom quiz, switch to this v1 flow unless you need include_webhook_payload, CRM/webhook on create, or funnel-host submission without an API key. The practical gains today are a faster create (matching is separate and cacheable), matching a product or collection without re-running the quiz, and a published funnel definition for your UI. Larger catalogs can keep using the same Match filters with bigger candidate sets later; that is an extension of this API, not a different product.
Product catalog import
- If you use Shopify, install our app to sync your products with our system.
- For Shopware and other platforms, take a look at the Product Feed for a description of how to provide a CSV feed that we will sync every 12 hours. To create a proof of concept, you can also provide us with a CSV file to import manually.
- For large product catalogs, we will soon provide an endpoint to import your products in batches and push catalog updates at scale.
What you can do with the data
Stella returns identifiers and questionnaire structure. You combine those with your own product data (titles, images, prices, stock, URLs).
| Surface | Example | How to use the API |
|---|---|---|
| Custom quiz | Native app or your own web form | GET the funnel, render steps, POST answers |
| Result page | Curated lists that match Stella tabs | GET matches with resultPage=true, hydrate shopSystemIds |
| Collection or search | “Your colours” badge or matching-only filter | GET matches with productId / variantId for the current page or a bounded set |
| Product page | Highlight matching colourways or shades | GET matches for that product’s IDs |
| Saved profile | Reuse one analysis across sessions | Store the analysis id against your customer or session |
Prices, availability, and purchase rules stay yours. Apply them at display time. If matching is slow or unavailable, keep normal shopping and omit optional badges.
Matching does not re-run image analysis. A second questionnaire submission creates a new analysis; Stella does not merge submissions into a customer account. Keep the link between your user/session and the analysis ID in your system.
The three operations
Your app → GET /api/v1/funnels/{funnelId} → steps for your quiz UI
→ POST /api/v1/analyses → analysis id (no recommendations)
→ GET /api/v1/analyses/{id}/matches → matching variant IDs
Your app ← your catalog (titles, prices, stock, URLs)| Operation | Auth | You send | Stella returns |
|---|---|---|---|
| Funnel definition | Company API Key (funnel.read) | Funnel ID, optional locale and version | Steps and option keys for that Funnel Version |
| Create analysis | Company API Key (analysis.create) | funnelId, answers, optional selfie | { id, funnelVersionId, isPreview } |
| Match variants | None (analysis ID is the capability) | Analysis ID, optional product/variant filters | matchedVariants (your shopSystemId strings) |
Matching is a separate GET so browsers and CDNs can cache it. Creating an analysis never returns recommended products and does not send CRM or webhooks.
Call these paths at https://api.askstella.ai/api/v1/... (the dashboard host serves the same routes if you already call app.askstella.ai).
Authentication
Funnel reads and analysis creates use a Company API Key. This is not the 25-character shop-component company key, not a dashboard session, and not a CRM webhook key.
- Get an invitation to the Stella platform from your Stella admin.
- In the Stella app, open Settings → Developer.
- Company Admins can create a key (name and optional expiry). The key value is shown once.
- Send it as a Bearer token. Do not put it in websites, mobile binaries, query strings, or logs.
Authorization: Bearer stella_ck_<key> Content-Type: application/json
Keys are scoped to one company (max 20 active). Default scopes are funnel.read and analysis.create. A key cannot read another company’s funnels. Rotation is create a new key, then revoke the old one.
| HTTP | error | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, malformed, revoked, or wrong-scope key |
| 401 | API_KEY_EXPIRED | expiresAt is in the past |
Keyed routes respond with Cache-Control: no-store. CORS allows Authorization and Idempotency-Key. Do not send cookies.
Match does not usethe authentication header. Anyone who knows an analysis ID can retrieve matching variant IDs for that analysis. The response does not include answers, Color Analysis, selfies, or contact data. Treat analysis IDs as unguessable capabilities (the same idea as a private result-page link).
GET /api/v1/funnels/{funnelId}
Loads the questionnaire for one funnel so you can render it yourself.
Query
| Parameter | Required | Meaning |
|---|---|---|
locale | No | Language tag for translated step copy. Defaults to the company’s language, with fallback to the stored default text. |
versionId | No | Funnel Version to return. Omit to use the current published version (or the current draft if nothing is published yet). Allowed values are only that published id or the current draft id. |
Example
GET /api/v1/funnels/cm1234567890?locale=en Authorization: Bearer stella_ck_…
{
"id": "cm1234567890",
"version": { "id": "cmversion1234567890", "number": 3 },
"steps": [
{
"id": 101,
"type": "HAIR_COLOR",
"orderNumber": 0,
"title": "What is your hair colour?",
"selections": [
{
"key": "BROWN",
"caption": "Brown",
"imageUrl": "https://cdn.askstella.ai/images/funnel/selections/hair-color-brown.jpg",
"orderNumber": 0
}
]
}
]
}steps is the stored Funnel Version configuration (camelCase JSON). Titles and custom option captions come from that version and locale fallback. Submit the option key, not the caption.
Built-in Color Analysis steps (hair, eyes, skin colour, undertone, selfie) are submitted as analysis scalars (hairColor, eyeColor, skinColor, selfie, …), not as selectionValues. Custom single- and multi-selection steps use selectionValues with stepId and selectedKey from this payload.
Omitted from every step: includeInGenericAnalysis, genericAnalysis, stepSettings, deleted, and per-step funnelId. Credentials and CRM settings are never returned.
Find the funnel id and version ids in the Stella dashboard (funnel Versions).
| HTTP | error | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown funnel, or funnel belongs to another company |
| 409 | FUNNEL_NOT_PUBLISHED | No published version and no draft |
| 400 | VERSION_NOT_FOUND | versionId is not the current published or draft version |
POST /api/v1/analyses
Creates an Analysis from quiz answers. Same answer fields as legacy POST /api/analyses, with these differences:
- Company API Key is required (
analysis.create). funnelIdis required (funnel-subdomain hosts are not used for v1).include_webhook_payloadis rejected (query or body).- No matching, CRM, or webhook delivery.
- Invalid or oversized selfies return
400before the image model runs. - Total JSON body must be at most 4 MiB; decoded selfie at most 2 MiB (JPEG, PNG, GIF, or WebP; prefer JPEG).
Omit createdWithVersionId to use the current published Funnel Version (or draft if none is published). Send the current draft id to create a preview analysis (isPreview: true)—that is the main reason to pass a version id. A superseded published id is still accepted: answers are migrated in memory to the current published version, funnelVersionId is that published id, and payload_warnings asks you to update the integration.
Do not send caller-chosen analysis IDs, company IDs, or calculated Color Analysis results. Color type is computed server-side and is not included in this response.
POST /api/v1/analyses Authorization: Bearer stella_ck_… Content-Type: application/json Idempotency-Key: quiz-completion-9001
{
"funnelId": "cm1234567890",
"createdWithVersionId": "cmversion1234567890",
"hairColor": "BROWN",
"eyeColor": "GREEN",
"skinColor": 30,
"selfie": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"selfieSource": "UPLOADED",
"selectionValues": [
{ "stepId": 101, "selectedKey": "DRY" },
{ "stepId": 202, "selectedKey": "ACNE" }
]
}Field meanings, enums, and selfie data-URL rules match the analyses API. v1 does not run frontend skincare-from-selfie analysis; skip that step or omit selfie for those funnels.
Success (200) — the analysis is persisted even when Color Analysis is null, and even when a selfie was sent but image analysis failed (no 503).
{
"id": "cmanalysis9001",
"funnelVersionId": "cmversion1234567890",
"isPreview": false
}Unexpected or missing answers usually still save the analysis and add payload_warnings (same behaviour as the legacy route). Handle that array so funnel changes do not go unnoticed.
{
"id": "cmanalysis9001",
"funnelVersionId": "cmversion1234567890",
"isPreview": false,
"payload_warnings": ["Funnel has a SKIN_COLOR step but skinColor is missing."]
}Idempotency-Key (optional). Without it, every call creates a new analysis. With it, the key is scoped to your company and this route. The same key and same body replay the first response. A different body with the same key returns 409 IDEMPOTENCY_CONFLICT. A retry while the first call is still running returns 409 REQUEST_IN_PROGRESS with Retry-After: 2. Raw selfie bytes are not stored on the idempotency record (the digest uses a hash of the selfie field).
| HTTP | error | When |
|---|---|---|
| 400 | INVALID_JSON | Body is not a JSON object |
| 400 | INVALID_REQUEST | Missing funnelId, or include_webhook_payload present |
| 400 | (legacy validation) | Unknown createdWithVersionId, invalid selfie (INVALID_SELFIE in the body) |
| 404 | NOT_FOUND | Funnel missing or not in this company |
| 409 | FUNNEL_NOT_PUBLISHED | Funnel has no version |
| 409 | IDEMPOTENCY_CONFLICT / REQUEST_IN_PROGRESS | Idempotency key reuse |
| 413 | REQUEST_TOO_LARGE | Body larger than 4 MiB |
GET /api/v1/analyses/{analysisId}/matches
Public, read-only matching. Resolves company and funnel from the analysis. Evaluates Recommended Variants with the same matcher as the Stella result page (tagging, complexion and custom tags, SRL, Funnel AI, Fixed Set lift), after the same in-memory answer migration the storefront uses. A later published Funnel Version does not make Match return 409. Color Analysis is not recomputed. The analysis row is not updated.
HEAD and OPTIONS are supported. HEAD checks the query and analysis and returns the same cache headers as a successful GET, without running the matcher. OPTIONS is side-effect-free.
GET /api/v1/analyses/cmanalysis9001/matches
Query (only these keys; anything else is 400 INVALID_QUERY)
| Parameter | Meaning |
|---|---|
Repeated variantId | Your Variant.shopSystemId values (exact, case-sensitive). |
Repeated productId | Your Product.shopSystemId values; expanded to eligible variants of those products. |
resultPage=true | Restrict to enabled result-page tabs and return tabs. Default is false. |
locale | Tab labels/descriptions. Defaults to the company language. |
IDs are deduplicated in request order. Within productId or variantId, values are OR’d. Product filters, variant filters, and result-page scope are AND’d. Omit filters to match the funnel’s full eligible assortment.
Unknown or out-of-scope IDs are omitted from the result (no “unresolved” field). Matcher failures return 5xx with MATCHER_FAILURE, never HTTP 200 with an empty list that hides an outage. A genuine empty match (nothing suitable) is 200 with "matchedVariants": [].
Preview / draft analyses are served so you can Match while implementing against draft. Those responses are not CDN-cached.
{
"matchedVariants": ["SKU-100-BLUE-M", "SKU-100-BLUE-L"]
}IDs are your shop-system variant IDs. Internal Stella primary keys are not returned as sellable IDs. The body does not include answers, selfies, contact fields, or product cards.
Result-page mode (resultPage=true) intersects matches with configured tabs. Products not on any tab are excluded (no fallback to the full assortment). If tabs are disabled or empty, both matchedVariants and tabs are []. tabs[].key is the tab’s CRM name. name / description are localized labels. maxItemsToShow is a presentation hint only; membership is not truncated. Null description / maxItemsToShow are omitted. matchedVariants is the union of tab variant lists in first-appearance order.
{
"matchedVariants": ["SKU-100-BLUE-M", "SKU-200-RED-S"],
"tabs": [
{ "key": "shirts", "name": "Shirts", "variants": ["SKU-100-BLUE-M"] },
{
"key": "dresses",
"name": "Dresses",
"description": "Your dresses",
"maxItemsToShow": 3,
"variants": ["SKU-200-RED-S"]
}
]
}Limits (explicit errors; no silent truncation)
| Limit | HTTP | error |
|---|---|---|
| Encoded query string > 8 KiB | 414 | URI_TOO_LONG |
More than 100 productId + variantId values, empty IDs, bad resultPage, unknown query keys | 400 | INVALID_QUERY |
| More than 2,000 eligible input variants for the request | 422 | MATCHING_SCOPE_TOO_LARGE |
| Serialized body > 1 MiB | 422 | MATCHING_RESPONSE_TOO_LARGE |
| Unknown analysis | 404 | NOT_FOUND |
For larger assortments, pass productId / variantId from the current page or a bounded collection rather than matching the entire catalog on every view.
Caching and CORS
Successful non-preview Match GET/HEAD responses:
Cache-Control: public, max-age=900, s-maxage=600, stale-while-revalidate Access-Control-Allow-Origin: *
That matches shop-component init: 15 minutes in the browser, 10 minutes on the CDN, then stale-while-revalidate. Product, tagging, and tab publishes invalidate Stella’s caches. Preview Matches and all error responses use Cache-Control: no-store.
Call Match with credential-free fetch (no cookies, no Authorization). CORS is *; origins are not reflected.
Two analyses with the same matching answers share Stella’s internal recommendation cache. Different companies or Funnel Versions do not.
Compared with shop components and the legacy analyses API
| Shop components | POST /api/analyses | v1 headless | |
|---|---|---|---|
| Quiz UI | Stella popup / embed | You or Stella | You |
| Auth | Company key in JS | Funnel host or platform | Company API Key (funnel + create) |
| Recommendations | Local storage / widget | Optional include_webhook_payload | Separate public Match GET |
| CRM / webhook | Unchanged | On complete | Not triggered by v1 create |
Keep using the webhook and legacy analyses API where you already depend on CRM payload shape or funnel-host submission. Do not mix include_webhook_payload into v1.