HTTP API
Public, read-only HTTP endpoints exposing your products, their widget configuration, and their associated audio reviews (audio URL, ratings, transcripts, author metadata). These are the same endpoints consumed by the Ekoo widget, made available for any read-only integration scenario: server-side rendering, native mobile applications, static site generation, internal tooling, data synchronization.
Authentication is not required. Requests and responses are JSON-encoded. Responses are cacheable at the Ekoo edge and on the client.
Common use cases
- Render product information, ratings, or transcripts in HTML prior to widget hydration — improving SEO indexability, accessibility, and first contentful paint.
- Display audio reviews inside a native mobile application alongside your product catalog.
- Pre-generate pages at build time using static site generators (Next.js ISR, Astro, Gatsby, Hugo).
- Synchronize product and review metadata into your data warehouse or search index.
Endpoints
| Method | Path | Returns |
|---|---|---|
| GET | /ws/websites/{websiteId}/products/{productRef} | A single product with its reviews and widget config. |
| GET | /ws/websites/{websiteId}/products | A paginated list of products. |
Base URL
1https://app.ekoo.coAll endpoints below are relative to this base URL. Responses are JSON (Content-Type: application/json).
Authentication
Requests are unauthenticated. The websiteId path parameter identifies the tenant; only resources owned by that website are returned. The websiteId is a public identifier and is already embedded in the widget snippet published on your site.
Your websiteId is available in the Ekoo backoffice under Settings → Website, or via the data-ekoo attribute of any page where the widget is installed.
Get a single product
Returns one product with its widget config and active audio reviews.
1GET /ws/websites/{websiteId}/products/{productRef}Path parameters
| Name | Type | Description |
|---|---|---|
websiteId | string (UUID) | Your Ekoo website identifier (same value as data-ekoo). |
productRef | string | Product reference as configured in the Ekoo backoffice. Case-sensitive. |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
locale | string | Optional | BCP-47 locale code (e.g. "fr", "en"). Filters reviews to the matching locale; falls back to all locales if no match. |
type | string | Optional | Widget type. Defaults to "standalone". |
variant | string | Optional | 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. |
Use 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.
Code samples
1curl -s "https://app.ekoo.co/ws/websites/{websiteId}/products/{productRef}?locale=en&variant=homepage"1// Node.js 18+ / Bun / Deno — global fetch2const websiteId = process.env.EKOO_WEBSITE_ID!3const productRef = 'SKU-1234'45const res = await fetch(6 `https://app.ekoo.co/ws/websites/${websiteId}/products/${productRef}?locale=en&variant=homepage`,7 { headers: { Accept: 'application/json' } }8)910if (!res.ok) throw new Error(`Ekoo API ${res.status}`)11const product = await res.json()1# Python 3.8+ — requests2import os, requests34website_id = os.environ["EKOO_WEBSITE_ID"]5product_ref = "SKU-1234"67res = requests.get(8 f"https://app.ekoo.co/ws/websites/{website_id}/products/{product_ref}",9 params={"locale": "en", "variant": "homepage"},10 timeout=5,11)12res.raise_for_status()13product = res.json()Example response
1{2 "id": "9b0c2f3e-8e4d-4d9b-9c8a-0f3a6b7e1d22",3 "external_id": "SKU-1234",4 "name": "Eau de parfum 50ml",5 "audio_url": "https://cdn.ekoo.co/audios/...",6 "website": {7 "id": "f7c1...",8 "name": "Acme Cosmetics"9 },10 "config": { /* widget config */ },11 "experiment": { /* optional A/B experiment */ },12 "reviews": [13 {14 "id": "5d2a...",15 "type": "review",16 "label": "Marie, 34",17 "rating": 5,18 "audio_url": "https://cdn.ekoo.co/audios/abc.mp3",19 "is_active": true,20 "is_default": true,21 "transcript": "I bought this perfume for my wife...",22 "user": {23 "id": "u_8f...",24 "firstname": "Marie",25 "lastname": "D.",26 "image_url": "https://cdn.ekoo.co/avatars/marie.jpg"27 }28 }29 ]30}Response fields
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Internal product identifier. |
external_id | string | Your product reference (same as productRef). |
name | string | Product name as configured in the backoffice. |
audio_url | string | Default audio URL (convenience field; usually equal to reviews[0].audio_url). |
website | object | Owning website (id, name). |
config | object | Resolved widget configuration (theme, CTA texts, animation, etc.). |
experiment | object | 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[].rating | number (0–5) | Star rating, when applicable. |
reviews[].audio_url | string | Audio file URL (mp3). |
reviews[].transcript | string | Text transcript of the audio. |
reviews[].is_default | boolean | True for the review the widget displays by default. |
reviews[].is_active | boolean | False for soft-deleted or disabled reviews. |
reviews[].user | object | Author metadata (firstname, lastname, image_url, description). |
TypeScript types
Add the following declarations to your project for fully typed responses. They cover both the single-product and list endpoints.
1// Drop into your project — fully typed responses23export type EkooReviewType = 'review' | 'testimony'45export interface EkooReviewUser {6 id: string7 firstname?: string8 lastname?: string9 email?: string10 image_url?: string11 description?: string12}1314export interface EkooReview {15 id: string16 type: EkooReviewType17 label?: string18 rating?: number19 audio_url?: string20 is_active: boolean21 is_default: boolean22 transcript?: string23 user: EkooReviewUser24}2526export interface EkooExperimentVariation {27 id: string28 name: string29 widgetEnabled: boolean30 widgetConfigId: string | null31 audioSelection: string32 audioId: string | null33 audioPosition: number | null34 audioIdsByLocale?: Record<string, string>35 allocation: number36 position: number37 config?: unknown38}3940export interface EkooExperiment {41 id: string42 name: string43 variations: EkooExperimentVariation[]44}4546export interface EkooProduct {47 id: string48 external_id: string49 name?: string50 audio_url?: string51 website: { id: string; name?: string }52 config?: Record<string, unknown>53 experiment?: EkooExperiment54 reviews: EkooReview[]55}5657export interface EkooProductList {58 limit: number59 offset: number60 count_item: number61 items: EkooProduct[]62}List products
Returns a paginated list of products for a website. Each item has the same shape as the single-product endpoint.
1GET /ws/websites/{websiteId}/productsQuery parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | Optional | Page size. Defaults to 50. Values ≤ 0 are coerced to 50. |
offset | integer | Optional | Result offset. Defaults to 0. |
without | string | Optional | Comma-separated list of product references to exclude from the response. |
Example request
1curl -s "https://app.ekoo.co/ws/websites/{websiteId}/products?limit=50&offset=0"Example response
1{2 "limit": 50,3 "offset": 0,4 "count_item": 124,5 "items": [6 { /* same shape as single product */ }7 ]8}Response fields
| Field | Type | Description |
|---|---|---|
limit | integer | Echo of the requested page size. |
offset | integer | Echo of the requested offset. |
count_item | integer | 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. |
To paginate, increment offset by limit until offset + items.length >= count_item.
Server-side rendering example (Next.js)
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.
1// app/products/[ref]/page.tsx — Next.js App Router (Server Component)2import { notFound } from 'next/navigation'3import type { EkooProduct } from '@/lib/ekoo-types'45async function fetchEkooProduct(ref: string, locale: string) {6 const url = `https://app.ekoo.co/ws/websites/${process.env.EKOO_WEBSITE_ID}/products/${ref}?locale=${locale}&variant=homepage`7 const res = await fetch(url, { next: { revalidate: 300 } })8 if (!res.ok) return null9 return (await res.json()) as EkooProduct10}1112export default async function ProductPage({13 params14}: { params: Promise<{ ref: string }> }) {15 const { ref } = await params16 const product = await fetchEkooProduct(ref, 'en')17 if (!product) notFound()1819 const primary = product.reviews.find((r) => r.is_default && r.is_active)2021 return (22 <article>23 {primary?.transcript && (24 <section aria-label="Audio review transcript">25 <h2>What our customers say</h2>26 <blockquote>{primary.transcript}</blockquote>27 <cite>{primary.user.firstname} {primary.user.lastname}</cite>28 </section>29 )}3031 {/* Ekoo widget hydrates client-side and takes over */}32 <ekoo-widget33 data-ekoo={process.env.EKOO_WEBSITE_ID}34 data-ekoo-product-id={ref}35 data-ekoo-locale="en"36 data-ekoo-variant="homepage"37 />38 </article>39 )40}CORS
Caching & rate limits
Responses are cached at the Ekoo edge and invalidated automatically when content is published from the backoffice. 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, purge the relevant page on your CDN; the Ekoo edge will already be serving the updated payload.
No per-key quota is enforced. Abusive traffic patterns — for instance, high-frequency uncached polling from a single IP — may be throttled at the edge. For sustained high request volume, contact support so capacity can be provisioned accordingly.
Errors
| Status | Meaning | When |
|---|---|---|
| 200 | OK | Resource found and returned. |
| 404 | Not found | Unknown websiteId or productRef, or product disabled. |
| 500 | Server error | Database or upstream failure. Safe to retry with exponential backoff. |
Error responses share a common shape:
1{2 "type": "ERR_NOT_FOUND",3 "message": "Product not found"4}The type field is a stable, machine-readable code. The message field is human-readable and may evolve over time; client logic should branch on type, not on message.
Versioning & stability
These endpoints power the public Ekoo widget and are maintained as a stable contract. New fields may be added to responses without prior notice; client implementations must ignore unknown fields. Existing fields are not renamed or removed without a documented migration path and a deprecation window.
Breaking changes, when unavoidable, are released under a new path prefix; the previous version remains available for a minimum of six months.
Related resources
- Widget Attributes Reference — data attributes the widget reads on the client.
- Widget Lifecycle — how the client widget hydrates over your SSR markup.
- Locales — supported values for the
localequery parameter.