API reference

Use direct APIs when your storefront needs deeper control.

The SDK covers most storefronts. These endpoints are documented for verification, catalog tooling, custom clients, and analytics integrations.

POST /api/widget/score

Scores quiz answers against a merchant catalog. Rate limit: 100/min.

POST /api/widget/event

Records widget lifecycle, click, lead, and purchase events. Rate limit: 1000/min.

POST /api/widget/verify

Validates a key and returns catalog readiness. Rate limit: 60/min.

POST /api/widget/lookup

Searches the Sillage perfume catalog for catalog tooling. Rate limit: 300/min.

Authentication and CORS

Score endpoint

NameTypeRequiredDefaultDescription
api_keystringYes-Widget API key in the request body.
quizWidgetQuizAnswersYes-Families, accord ratings, season, and occasion answers.
session_idstringNo-Optional stable session ID. The server creates one when omitted.
catalog_mode'synced' | 'inline'Nosynced'synced' means connected catalog products linked to the API key. 'inline' means page products passed in this request.
inline_catalogMerchantProductInput[]No-Required only when catalog_mode is 'inline' / page products. Each product needs merchant_product_id, name, and house; scent fields are optional Sillage-owned hints.
limitnumberNo6Maximum recommendation count.
locale'ar' | 'en'NoarLocale used for downstream widget behavior.

Request

json

{
  "api_key": "sw_test_your_key",
  "quiz": {
    "families": ["woody", "oud"],
    "accord_ratings": {
      "oud": "love",
      "citrus": "avoid"
    },
    "season": "winter",
    "occasion": "date-night"
  },
  "catalog_mode": "synced",
  "limit": 6,
  "locale": "en"
}

Response

json

{
  "session_id": "0e98a06b-8e9f-42d7-85e5-0dd8a7d95f31",
  "merchant": { "id": "...", "name": "Maison Parfum", "slug": "maison-parfum" },
  "recommendations": [
    {
      "merchant_product_id": "oud-velvet-50ml",
      "sillage_perfume_id": "perfume-id",
      "name": "Velvet Oud",
      "house": "Maison Parfum",
      "match_score": 94,
      "match_accords": ["oud", "woody", "amber"],
      "quality": "full",
      "price": 420,
      "currency": "SAR",
      "product_url": "https://store.example/products/velvet-oud"
    }
  ],
  "also_explore": [],
  "taste_summary": {
    "dominant_families": ["woody", "oud"],
    "top_accords": ["oud", "woody", "amber"],
    "avoid_accords": ["citrus"]
  },
  "powered_by": {
    "label": "Powered by Sillage",
    "url": "https://mysillage.co/try?ref=widget&merchant=maison-parfum&session=...",
    "logo_svg": "<svg>...</svg>"
  },
  "test_mode": true
}

Merchant product input

Merchants should not be asked to provide accords or note pyramids. Send product identity and commerce metadata; Sillage matches, normalizes, and enriches the olfactive profile.

NameTypeRequiredDefaultDescription
merchant_product_idstringYes-Stable SKU or product ID from the merchant platform.
namestringYes-Product/fragrance name. Sillage uses this with house to match and enrich the perfume.
housestringYes-Brand or perfume house.
pricenumberNo-Optional display price.
currencystringNoSAROptional ISO currency code.
image_urlstringNo-Optional product image. Sillage can fall back to the matched perfume image.
product_urlstringNo-Optional product page URL for recommendation clicks.
accordsstring[]No-Optional hint only. Sillage does not expect merchants to maintain accords.
notes{ top?: string[]; heart?: string[]; base?: string[] }No-Optional hint only. Sillage enriches notes when the product is matched or grounded.

Error handling best practices

Treat the widget as a conversion aid, not a critical checkout dependency. If the API is unavailable, hide the widget container or show a gentle fallback instead of blocking the storefront.

Fetch with rate-limit handling

js

async function scoreWithFallback(payload) {
  const response = await fetch('https://mysillage.co/api/widget/score', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Widget-Version': 'v1' },
    body: JSON.stringify(payload)
  });

  if (response.status === 429) {
    const retryAfter = response.headers.get('Retry-After') ?? '60';
    throw new Error(`Sillage is busy. Try again in ${retryAfter} seconds.`);
  }

  if (!response.ok) {
    const body = await response.json().catch(() => ({}));
    throw new Error(body.error ?? 'Recommendations are unavailable.');
  }

  return response.json();
}

400

Validate quiz payloads before calling score.

401 / 403

Check API key, revocation state, and allowed origins.

429

Respect Retry-After and avoid automatic tight retry loops.