HTTP API
Retrieve your products, widget configuration, and audio content from a public, read-only API. All responses use JSON.
- Base URL
- https://app.ekoo.co
- Authentication
- None
- Format
- JSON
- Access
- Public · read-only
Before you start
No authentication is required. The only identifier you need is your website ID.
curl -s "https://app.ekoo.co/ws/websites/{websiteId}/products?limit=5" \ -H "Accept: application/json"The websiteId is a public identifier available under Settings → Website in the backoffice, or in the data-ekoo attribute of the widget already installed on your pages.
Common use cases
- Render product data and transcripts server-side
- SEO
- Display audio content in a native mobile application
- Apps
- Generate static pages or synchronize an external catalog
- Batch
Get a product
Returns one product with its widget configuration and active audio content.
/ws/websites/{websiteId}/products/{productRef}curl -s "https://app.ekoo.co/ws/websites/{websiteId}/products/{productRef}" \ -H "Accept: application/json"Request
GET https://app.ekoo.co/ws/websites/{websiteId}/products/{productRef}Example responseFull JSON shape, without running the request
{ "id": "9b0c2f3e-8e4d-4d9b-9c8a-0f3a6b7e1d22", "external_id": "SKU-1234", "name": "Eau de parfum 50ml", "seo_name": "Eau de parfum 50ml — Acme", "seo_description": "Les avis audio de nos clients.", "audio_url": "https://cdn.ekoo.co/audios/...", "website": { "id": "f7c1...", "name": "Acme Cosmetics" }, "features": { "ctaAnalytics": true }, "config": { /* widget configuration */ }, "experiment": { /* optional A/B experiment */ }, "reviews": [ { "id": "5d2a...", "type": "review", "label": "Marie, 34", "rating": 5, "audio_url": "https://cdn.ekoo.co/audios/abc.mp3", "is_active": true, "is_default": true, "created_at": "2026-03-14T09:12:00.000Z", "transcript": "I bought this perfume for my wife...", "user": { "id": "u_8f...", "firstname": "Marie", "lastname": "D.", "image_url": "https://cdn.ekoo.co/avatars/marie.jpg" } } ]}Path parameters
websiteIdstring (UUID)- Your Ekoo website identifier, the same value as data-ekoo.
productRefstring- Product reference as configured in the Ekoo backoffice. Case-sensitive.
Query parameters
localestringoptional- BCP-47 locale code (e.g. “fr”, “en”). Strictly filters reviews to that locale. When omitted, all locales are returned.
typestringoptional- Widget type. Defaults to “standalone”.
variantstringoptional- Stable reference of a widget configuration. The product and its audios are still resolved from
productRef; only the widget styling changes. Legacy compatibility: if no ref matches, the backend still tries a name match.
Choosing between productRef and variant
variant to reuse the same product with multiple widget skins without duplicating its audios or reviewers. productRef still owns the product data; variant only selects the widget configuration.Response fields
idstring (UUID)- Internal product identifier.
external_idstring- Your product reference, same as productRef.
namestring- Product name as configured in the backoffice.
seo_namestring- SEO-optimised name, when set in the backoffice.
seo_descriptionstring- SEO-optimised description, when set.
audio_urlstring- Default audio URL. Convenience field, usually equal to reviews[0].audio_url.
websiteobject- Owning website (id, name).
featuresobject- Options enabled for this website, read by the widget. Currently holds ctaAnalytics.
configobject- Resolved widget configuration: theme, CTA texts, animation, etc.
experimentobject | undefined- Present only if an A/B experiment is running on this product.
reviews[]array- Active audio reviews and testimonies attached to the product.
reviews[].type"review" | "testimony"- Whether the audio is a product review or a testimony.
reviews[].ratingnumber (0–5)- Star rating, when applicable.
reviews[].audio_urlstring- Audio file URL (mp3).
reviews[].transcriptstring- Text transcript of the audio.
reviews[].is_defaultboolean- True for the review the widget displays by default.
reviews[].is_activeboolean- False for soft-deleted or disabled reviews.
reviews[].created_atstring (ISO 8601)- Creation date of the audio.
reviews[].userobject- Author metadata: firstname, lastname, image_url, description.
TypeScript typesDeclarations covering both endpoints
// Copy into your project — full response typings export type EkooReviewType = 'review' | 'testimony' export interface EkooReviewUser { id: string firstname?: string lastname?: string image_url?: string description?: string} export interface EkooReview { id: string type: EkooReviewType label?: string rating?: number audio_url?: string is_active: boolean is_default: boolean transcript?: string created_at?: string user: EkooReviewUser} export interface EkooExperimentVariation { id: string name: string widgetEnabled: boolean widgetConfigId: string | null audioSelection: string audioId: string | null audioPosition: number | null audioIdsByLocale?: Record<string, string> allocation: number position: number config?: unknown} export interface EkooExperiment { id: string name: string variations: EkooExperimentVariation[]} export interface EkooProduct { id: string external_id: string name?: string seo_name?: string seo_description?: string audio_url?: string website: { id: string; name?: string } features?: { ctaAnalytics: boolean } config?: Record<string, unknown> experiment?: EkooExperiment reviews: EkooReview[]} export interface EkooProductList { limit: number offset: number count_item: number items: EkooProduct[]}List products
Returns a paginated list of website products. Filters are applied before pagination and the total count.
/ws/websites/{websiteId}/productscurl -s "https://app.ekoo.co/ws/websites/{websiteId}/products?limit=5&locale=en" \ -H "Accept: application/json"Request
GET https://app.ekoo.co/ws/websites/{websiteId}/products?limit=5&locale=enExample responseFull JSON shape, without running the request
{ "limit": 50, "offset": 0, "count_item": 124, "items": [ { /* same shape as a single product */ } ]}Query parameters
limitintegeroptional- Page size. Defaults to 50. Values ≤ 0 are coerced to 50.
offsetintegeroptional- Result offset. Defaults to 0.
withoutstringoptional- Special “inactive” value that excludes inactive audio content from the response.
localestringoptional- Locale of the audio content being filtered and returned. Defaults to “fr”.
withAudiobooleanoptional- Filters products by whether at least one returnable audio exists in the requested locale: non-empty URL, validated, and not awaiting validation.
publishedbooleanoptional- Filters products by whether at least one audio is published: non-empty URL, active, validated, and not awaiting validation.
Common audio filters
- Products with at least one validated audio in the requested locale, active or inactive
withAudio=true- Products with at least one published audio usable by the widget
withAudio=true&published=true- Products with at least one returnable audio but no published audio
withAudio=true&published=false- Products without any returnable audio in the requested locale
withAudio=false
Filtering and pagination
limit, offset, and the count_item calculation. Do not filter client-side using audio_url. When used alone, published=false also includes products without audio; combine it with withAudio=true to return only products that have returnable but unpublished audio.Response fields
limitinteger- Echo of the requested page size.
offsetinteger- Echo of the requested offset.
count_iteminteger- Total number of products matching the query, across all pages.
items[]array- Page of products. Each item has the same shape as the single-product response.
Server-side rendering
Render Ekoo content into your own HTML, then let the widget hydrate on top.
// app/products/[ref]/page.tsx — Next.js App Router (Server Component)import { notFound } from 'next/navigation'import type { EkooProduct } from '@/lib/ekoo-types' async function fetchEkooProduct(ref: string, locale: string) { const url = `https://app.ekoo.co/ws/websites/${process.env.EKOO_WEBSITE_ID}/products/${ref}?locale=${locale}&variant=homepage` const res = await fetch(url, { next: { revalidate: 300 } }) if (!res.ok) return null return (await res.json()) as EkooProduct} export default async function ProductPage({ params}: { params: Promise<{ ref: string }> }) { const { ref } = await params const product = await fetchEkooProduct(ref, 'en') if (!product) notFound() const primary = product.reviews.find((r) => r.is_default && r.is_active) return ( <article> {primary?.transcript && ( <section aria-label="Audio review transcript"> <h2>What our customers say</h2> <blockquote>{primary.transcript}</blockquote> <cite>{primary.user.firstname} {primary.user.lastname}</cite> </section> )} {/* The Ekoo widget hydrates on the client and takes over */} <ekoo-widget data-ekoo={process.env.EKOO_WEBSITE_ID} data-ekoo-product-id={ref} data-ekoo-locale="en" data-ekoo-variant="homepage" /> </article> )}Fetch the product on the server, render the required fields — rating, transcript, author, custom layout — into your HTML, and let the Ekoo widget hydrate on top once it loads. The pattern is identical in Nuxt (useFetch), SvelteKit (+page.server.ts), Astro, Remix, or any backend capable of issuing an HTTP request.
CORS
Errors
All error responses share a common shape.
{ "type": "ERR_DB_MISSING", "message": "error-message-not-found"}Check the body, not just the status
GET /products/{productRef}, errors are returned with a 200 status — long-standing behaviour requested by integrators. An unknown product is therefore only detectable from the type field in the response body.Status codes
200OK- Resource found. On the single-product endpoint, also returned on errors: check the type field.
400Bad request- Invalid query parameter, on the list endpoint.
500Server error- Database or upstream failure. Safe to retry with exponential backoff.
Error body
typestring- Stable, machine-readable code. Client logic should branch on this field. Common values: ERR_DB_MISSING (unknown product or website), ERR_INPUT_VALIDATION (invalid parameter), ERR_DB_READ (database failure).
messagestring- Human-readable message. May evolve over time.
Caching and limits
What you can rely on in production.
Caching
Individual product responses may be cached by Ekoo, and that cache is invalidated when content is updated from the backoffice. The list response should not be assumed to be cached. We recommend layering an additional short-lived cache on the client side — for example revalidate: 300 in Next.js, s-maxage on a CDN — to absorb traffic spikes and mitigate upstream incidents.
When new content must be served before the next revalidation, also purge the relevant page on your own CDN.
Rate limits
The API does not use authentication keys and currently publishes no contractual quota. Avoid high-frequency uncached polling. For sustained high request volume, contact support so capacity can be provisioned accordingly.