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
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| api_key | string | Yes | - | Widget API key in the request body. |
| quiz | WidgetQuizAnswers | Yes | - | Families, accord ratings, season, and occasion answers. |
| session_id | string | No | - | Optional stable session ID. The server creates one when omitted. |
| catalog_mode | 'synced' | 'inline' | No | synced | 'synced' means connected catalog products linked to the API key. 'inline' means page products passed in this request. |
| inline_catalog | MerchantProductInput[] | 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. |
| limit | number | No | 6 | Maximum recommendation count. |
| locale | 'ar' | 'en' | No | ar | Locale 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.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| merchant_product_id | string | Yes | - | Stable SKU or product ID from the merchant platform. |
| name | string | Yes | - | Product/fragrance name. Sillage uses this with house to match and enrich the perfume. |
| house | string | Yes | - | Brand or perfume house. |
| price | number | No | - | Optional display price. |
| currency | string | No | SAR | Optional ISO currency code. |
| image_url | string | No | - | Optional product image. Sillage can fall back to the matched perfume image. |
| product_url | string | No | - | Optional product page URL for recommendation clicks. |
| accords | string[] | 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.