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.

GET/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}
Required: websiteId, 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

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.

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.

GET/ws/websites/{websiteId}/products
curl -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=en
Required: websiteId
Example 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

Filters are applied before 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

All origins are allowed. The API may be called from a browser or from a server, though server-side calls are generally preferable: responses can be cached, payloads do not transit through the end user's network, and an additional round-trip on first paint is avoided.

Errors

All error responses share a common shape.

{
"type": "ERR_DB_MISSING",
"message": "error-message-not-found"
}
⚠️

Check the body, not just the status

On 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.