SEO Signals API
The SEO Signals API is the endpoint headless storefronts use to fetch Clione's enriched SEO/AEO/GEO signals for one entity during server-side rendering. It's also what the @clione/seo SDK calls in API mode.
Use it when:
- You're building a headless / custom storefront and want to render Clione's signals in your own template.
- You don't want to install an embed widget or theme app block.
- You need fine-grained control over which signals you read and where you render them.
For non-headless stores (BigCommerce Stencil or Shopify Liquid themes), you don't need this endpoint: publishing from Clione delivers the JSON-LD server-side — through a widget on BigCommerce and the theme app embeds on Shopify. See Signal propagation.
Endpoint
GET /api/v1/public/seo-signals/:platform/:entityType/:entityId
Path parameters:
| Param | Values |
|---|---|
platform | bigcommerce | shopify | woocommerce |
entityType | product | category | collection | page |
entityId | The entity's Clione id, as returned by the Products API. For products, the platform's numeric product id also works. On Shopify, the entity's handle also works. |
Query parameters:
| Param | Notes |
|---|---|
storeId | Optional. Pins the lookup to one store. Defaults to the store the API key is bound to. A Shopify handle that exists in more than one of your stores returns 404 with code: "AMBIGUOUS_HANDLE" until you pass it or use a store-bound key. |
Authentication
Send an API key with the products:read scope in one of two headers:
Authorization: Bearer sk_live_...
X-API-Key: sk_live_...
The organization is taken from the key. If the key has allowed domains, the request must name one of them: a server key declares it in the X-Clione-Storefront-Host header, a browser key through Origin/Referer. For SSR, use a server key — see Authentication.
Response shape
{
"jsonLd": {
"@context": "https://schema.org",
"@graph": [
{ "@type": "Organization", "...": "..." },
{ "@type": "WebSite", "...": "..." },
{ "@type": "BreadcrumbList", "...": "..." },
{ "@type": "Product", "name": "Linen Bed Sheet Set", "...": "..." },
{ "@type": "FAQPage", "mainEntity": ["..."] }
]
},
"metaTitle": "Linen Bed Sheet Set — Cool, Breathable, Made in Portugal",
"metaDescription": "Pure European linen sheet set ideal for hot sleepers... — linen sheets, breathable bedding",
"keywords": ["linen sheets", "breathable bedding", "summer bedding"],
"faq": [{ "question": "...", "answer": "..." }]
}
| Field | Type | Notes |
|---|---|---|
jsonLd | object | null | The schema.org @graph Clione publishes for this entity on the platform's own storefronts — the same graph for products, categories, collections and pages. Render it verbatim in one <script type="application/ld+json">. |
metaTitle | string | null | For products, the meta title Clione pushes. For other entity types, the entity name. |
metaDescription | string | null | Up to 320 characters; may end with the top search keywords. |
keywords | string[] | Search keywords from enrichment. Empty array when there are none. |
faq | array | null | Approved FAQ pairs, or null when the entity has none. |
Example — fetch
const res = await fetch(
`${process.env.CLIONE_API_URL}/api/v1/public/seo-signals/shopify/product/1234`,
{
headers: {
Authorization: `Bearer ${process.env.CLIONE_API_KEY}`,
'X-Clione-Storefront-Host': 'shop.example.com',
},
},
)
if (!res.ok) throw new Error(`Clione SEO Signals: ${res.status}`)
const { jsonLd, metaTitle, metaDescription, keywords, faq } = await res.json()
Example — curl
curl -s \
-H "Authorization: Bearer sk_live_..." \
-H "X-Clione-Storefront-Host: shop.example.com" \
"https://api.clione.ai/api/v1/public/seo-signals/shopify/product/1234" | jq .
Rendering
Map the fields onto your framework's head API: metaTitle → <title>, metaDescription → <meta name="description">, jsonLd → one <script type="application/ld+json">. The @clione/seo SDK does this for Next.js, Remix / Hydrogen and Nuxt, and the guides for Next.js and Hydrogen walk through it.
Caching
Successful responses carry Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400. Signals only change when an entity is re-enriched or edited, so caching on your edge (Cloudflare, Vercel) or in your framework is safe.
Rate limit
200 requests per 5 minutes per API key. Responses carry the standard RateLimit-* headers. Over the limit, the API answers 429 with a retryAfter value in seconds.
Error responses
| Status | Cause |
|---|---|
| 400 | Invalid platform or entityType. |
| 401 | Missing, invalid, inactive or expired API key. |
| 403 | Missing products:read scope, host not in the key's allowed domains, or a browser key with allowed domains used from a non-browser client. |
| 404 | Entity not found: wrong id, not synced, or, on Shopify, a handle that matches nothing. The body carries error plus the empty signal fields. |
| 429 | Rate limited. Wait retryAfter seconds. |
| 500 | Server error. Retry with backoff. |
Troubleshooting
Returns 404 even though I see the product in my dashboard — Check that the id is the one the Products API returns. On Shopify, a handle shared by two of your stores needs ?storeId= or a store-bound key.
JSON-LD missing aggregateRating — Reviews are not enabled in your platform, or no product has ratings yet. See Reviews setup.