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

MethodPathReturns
GET/ws/websites/{websiteId}/products/{productRef}A single product with its reviews and widget config.
GET/ws/websites/{websiteId}/productsA paginated list of products.

Base URL

1https://app.ekoo.co

All 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

NameTypeDescription
websiteIdstring (UUID)Your Ekoo website identifier (same value as data-ekoo).
productRefstringProduct reference as configured in the Ekoo backoffice. Case-sensitive.

Query parameters

NameTypeRequiredDescription
localestringOptionalBCP-47 locale code (e.g. "fr", "en"). Filters reviews to the matching locale; falls back to all locales if no match.
typestringOptionalWidget type. Defaults to "standalone".
variantstringOptionalStable 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

curl
bash
1curl -s "https://app.ekoo.co/ws/websites/{websiteId}/products/{productRef}?locale=en&variant=homepage"
Node.js / Bun / Deno
typescript
1// Node.js 18+ / Bun / Deno — global fetch
2const websiteId = process.env.EKOO_WEBSITE_ID!
3const productRef = 'SKU-1234'
4
5const res = await fetch(
6 `https://app.ekoo.co/ws/websites/${websiteId}/products/${productRef}?locale=en&variant=homepage`,
7 { headers: { Accept: 'application/json' } }
8)
9
10if (!res.ok) throw new Error(`Ekoo API ${res.status}`)
11const product = await res.json()
Python
python
1# Python 3.8+ — requests
2import os, requests
3
4website_id = os.environ["EKOO_WEBSITE_ID"]
5product_ref = "SKU-1234"
6
7res = 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

200 OK
json
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

FieldTypeDescription
idstring (UUID)Internal product identifier.
external_idstringYour product reference (same as productRef).
namestringProduct name as configured in the backoffice.
audio_urlstringDefault audio URL (convenience field; usually equal to reviews[0].audio_url).
websiteobjectOwning website (id, name).
configobjectResolved widget configuration (theme, CTA texts, animation, etc.).
experimentobject | undefinedPresent only if an A/B experiment is running on this product.
reviews[]arrayActive 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_urlstringAudio file URL (mp3).
reviews[].transcriptstringText transcript of the audio.
reviews[].is_defaultbooleanTrue for the review the widget displays by default.
reviews[].is_activebooleanFalse for soft-deleted or disabled reviews.
reviews[].userobjectAuthor 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.

ekoo-types.ts
typescript
1// Drop into your project — fully typed responses
2
3export type EkooReviewType = 'review' | 'testimony'
4
5export interface EkooReviewUser {
6 id: string
7 firstname?: string
8 lastname?: string
9 email?: string
10 image_url?: string
11 description?: string
12}
13
14export interface EkooReview {
15 id: string
16 type: EkooReviewType
17 label?: string
18 rating?: number
19 audio_url?: string
20 is_active: boolean
21 is_default: boolean
22 transcript?: string
23 user: EkooReviewUser
24}
25
26export interface EkooExperimentVariation {
27 id: string
28 name: string
29 widgetEnabled: boolean
30 widgetConfigId: string | null
31 audioSelection: string
32 audioId: string | null
33 audioPosition: number | null
34 audioIdsByLocale?: Record<string, string>
35 allocation: number
36 position: number
37 config?: unknown
38}
39
40export interface EkooExperiment {
41 id: string
42 name: string
43 variations: EkooExperimentVariation[]
44}
45
46export interface EkooProduct {
47 id: string
48 external_id: string
49 name?: string
50 audio_url?: string
51 website: { id: string; name?: string }
52 config?: Record<string, unknown>
53 experiment?: EkooExperiment
54 reviews: EkooReview[]
55}
56
57export interface EkooProductList {
58 limit: number
59 offset: number
60 count_item: number
61 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}/products

Query parameters

NameTypeRequiredDescription
limitintegerOptionalPage size. Defaults to 50. Values ≤ 0 are coerced to 50.
offsetintegerOptionalResult offset. Defaults to 0.
withoutstringOptionalComma-separated list of product references to exclude from the response.

Example request

curl
bash
1curl -s "https://app.ekoo.co/ws/websites/{websiteId}/products?limit=50&offset=0"

Example response

200 OK
json
1{
2 "limit": 50,
3 "offset": 0,
4 "count_item": 124,
5 "items": [
6 { /* same shape as single product */ }
7 ]
8}

Response fields

FieldTypeDescription
limitintegerEcho of the requested page size.
offsetintegerEcho of the requested offset.
count_itemintegerTotal number of products matching the query (across all pages).
items[]arrayPage 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.

app/products/[ref]/page.tsx
tsx
1// app/products/[ref]/page.tsx — Next.js App Router (Server Component)
2import { notFound } from 'next/navigation'
3import type { EkooProduct } from '@/lib/ekoo-types'
4
5async 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 null
9 return (await res.json()) as EkooProduct
10}
11
12export default async function ProductPage({
13 params
14}: { params: Promise<{ ref: string }> }) {
15 const { ref } = await params
16 const product = await fetchEkooProduct(ref, 'en')
17 if (!product) notFound()
18
19 const primary = product.reviews.find((r) => r.is_default && r.is_active)
20
21 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 )}
30
31 {/* Ekoo widget hydrates client-side and takes over */}
32 <ekoo-widget
33 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

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.

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

StatusMeaningWhen
200OKResource found and returned.
404Not foundUnknown websiteId or productRef, or product disabled.
500Server errorDatabase 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

Ekoo HTTP API — Documentation — Ekoo