API développeur
Une API en lecture seule vers l'assistant de votre propre boutique : réponses, suggestions pendant la frappe, produits similaires et catalogue, pour votre serveur, votre appli ou votre page.
Version de l'API 1.0.0
Vue d'ensemble#
L'API donne à votre code ce que l'assistant du site donne à vos acheteurs : une réponse complète à une question en langage courant, des suggestions de produits pendant la frappe, des listes de produits similaires et la lecture du catalogue. URL de base : https://assistant.onesite.agency/api/v1.
Elle est en lecture seule et limitée à une boutique : une clé ouvre la boutique qui l'a créée, rien d'autre. Noms, prix, stocks et liens viennent toujours du catalogue ; l'IA n'écrit que les courts textes say et why, et la réponse les signale (voir « Texte généré par IA » plus bas).
Requêtes et réponses sont du JSON sur HTTPS. La version figure dans le chemin (/v1). L'API est disponible sur les forfaits qui incluent l'API développeur.
Démarrage rapide#
De zéro à une première réponse en trois étapes.
1. Créez une clé#
Dans le cabinet, ouvrez l'onglet Developers, nommez la clé et choisissez ses scopes, puis éventuellement une liste d'IP et des origines de navigateur. La clé n'est affichée qu'une fois et nous n'en gardons qu'une empreinte : copiez-la directement dans les secrets de votre serveur, jamais dans un dépôt. Une clé perdue ne se récupère pas : révoquez-la et créez-en une autre.
2. Vérifiez-la#
Appelez GET /me. Un 200 indique quelle boutique la clé ouvre, ses scopes et ses limites. Remplacez YOUR_API_KEY par votre clé.
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/me' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/me", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/me",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())3. Posez une question#
Envoyez les mots de l'acheteur à POST /answers (scope answer requis). Vous recevez le meilleur choix, des alternatives et une raison en une ligne pour chacun.
Exemple de requête, curl
curl -X POST 'https://assistant.onesite.agency/api/v1/answers' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa' \
-d '{
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": true,
"session_id": "visitor-8841"
}'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/answers", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa",
},
body: JSON.stringify({
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": true,
"session_id": "visitor-8841"
}),
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.post(
"https://assistant.onesite.agency/api/v1/answers",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa",
},
json={
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": True,
"session_id": "visitor-8841",
},
)
print(response.status_code, response.json())Authentification#
Envoyez la clé dans l'en-tête Authorization comme jeton bearer. Ne la mettez jamais dans l'URL : les URL finissent dans les journaux et l'historique du navigateur.
Authentification
Authorization: Bearer YOUR_API_KEYUne clé absente, mal formée, inconnue ou révoquée donne toujours le même 401 invalid_api_key : la réponse ne dit jamais à qui devine qu'une clé est presque juste. Les clés invalides répétées depuis une adresse sont freinées par 429 too_many_failed_attempts.
Une clé appartient à une seule boutique et ne peut pas en lire une autre : l'identifiant d'un produit d'une autre boutique répond 404, comme un produit inexistant.
Vous pouvez restreindre davantage une clé avec une liste d'IP autorisées (403 ip_not_allowed) et une liste d'origines de navigateur (403 origin_not_allowed). Les scopes sont fixés à la création de la clé ; pour les élargir, créez une autre clé.
Scopes et limites#
Chaque clé porte les scopes qu'elle peut utiliser. Les limites s'appliquent par clé et par scope.
| Scope | Endpoints | Appels par 60 s |
|---|---|---|
answer | POST /answers | 20 |
suggest | GET /suggestions | 120 |
catalogue:read | GET /products, GET /products/{product_id} | 120 |
similar | GET /products/{product_id}/similar | 60 |
analytics:read | GET /analytics/summary, GET /analytics/series, GET /analytics/queries, GET /analytics/sales | 30 |
POST /answers a en plus un plafond quotidien de 500 appels par clé (jour UTC), car chaque réponse est un appel au modèle. Un rejeu avec le même Idempotency-Key ne compte pas.
Chaque réponse porte X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset ; un 429 ajoute Retry-After (secondes). Attendez ce délai plutôt que de réessayer en boucle. GET /me exige une clé valide mais aucun scope particulier.
Endpoints#
Chaque opération ci-dessous est construite à partir du document OpenAPI, et chaque exemple est exécuté contre la vraie API par nos tests : les exemples ne peuvent donc pas s'écarter de l'API. Les descriptions des opérations et des champs sont en anglais, comme dans le document.
GET /me#
Vérifiez quelle boutique, quels scopes et quelles limites une clé possède.
Describes the key you are calling with: its scopes, shop, allow-lists and limits. Costs no quota; use it to check a key.
- Scope requis
- toute clé valide
Aucun paramètre.
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | KeyInfo | Successful Response |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Check a key (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/me' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/me", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/me",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"id": 3,
"name": "Storefront search",
"hint": "osk_…PLEE",
"scopes": [
"analytics:read",
"answer",
"catalogue:read",
"similar",
"suggest"
],
"shop": {
"domain": "alpha.example",
"currency": "EUR",
"language": "en"
},
"origins": [],
"networks": [],
"created_at": "2026-10-13T09:00:00+00:00",
"last_used_at": null,
"limits": {
"answer": {
"per_minute": 20
},
"suggest": {
"per_minute": 120
},
"similar": {
"per_minute": 60
},
"catalogue:read": {
"per_minute": 120
},
"analytics:read": {
"per_minute": 30
}
},
"answers_per_day": 500
}Missing or invalid key (HTTP 401)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/me'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/me", {
method: "GET",
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/me",
)
print(response.status_code, response.json())Exemple de réponse · HTTP 401
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "Missing or invalid API key. Send it as 'Authorization: Bearer osk_…'.",
"param": null,
"request_id": "req_9f2b000000000001"
}
}POST /answers#
Posez une question avec les mots de l'acheteur et obtenez le meilleur choix, des alternatives et une raison pour chacun.
Answers a shopper's question the way the shop's own assistant does: the best match, alternatives and a one-line reason for each. Product facts (title, price, stock, link) always come from the shop's catalogue; only say and why are written by an AI model — see the ai object, and pass that on to your users.
Counted against the key's per-minute limit and its daily cap; a replay with the same Idempotency-Key is free.
- Scope requis
answer- Limite
- 20 appels par 60 s
| Nom | Où | Type | Obligatoire | Contraintes | Description |
|---|---|---|---|---|---|
Idempotency-Key | header | string | null | non | longueur 1–64 | Makes a retry safe: the same key and body return the first answer without a second model call. |
Corps de la requête: AnswerRequest#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
query | string | oui | The shopper's question, in their own words. |
language | Language | null | non | Language of the reply (ru en el es de it fr). Defaults to the shop's language. |
session_id | string | null | non | Opaque id of one conversation. Send the same id to let follow-ups (“cheaper”, “something lighter”) build on earlier turns. Sessions are private to your API key. |
in_stock_only | boolean | non | Only products that can be bought now (excludes pre-orders). |
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | Answer | Successful Response |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
409 | ErrorBody | Same Idempotency-Key is still being processed. |
422 | ErrorBody | Invalid request body, or Idempotency-Key reused with a different body. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
502 | ErrorBody | The answer could not be produced right now. Safe to retry. |
A shopper asks in their own words (HTTP 200)#
Exemple de requête, curl
curl -X POST 'https://assistant.onesite.agency/api/v1/answers' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa' \
-d '{
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": true,
"session_id": "visitor-8841"
}'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/answers", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa",
},
body: JSON.stringify({
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": true,
"session_id": "visitor-8841"
}),
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.post(
"https://assistant.onesite.agency/api/v1/answers",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa",
},
json={
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": True,
"session_id": "visitor-8841",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"id": "6f1b4a52-3c0d-4d5e-9a7e-2b8c1d0f4e11",
"query": "a cold, quiet science fiction novel for a long winter",
"intent": "search",
"say": "Here is the best match for a cold, quiet read.",
"main": {
"product": {
"id": 101,
"title": "The Left Hand of Darkness",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/101",
"handle": "the-left-hand-of-darkness",
"external_id": "gid://shop/101",
"price": {
"amount": "12.90",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/101.jpg",
"thumb_url": "https://cdn.alpha.example/101-s.jpg"
},
"why": "A quiet, snowbound classic - the cold-world mood you described, in one of the genre's best-loved novels.",
"why_source": "model"
},
"alternatives": [
{
"product": {
"id": 102,
"title": "The Dispossessed",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/102",
"handle": "the-dispossessed",
"external_id": "gid://shop/102",
"price": {
"amount": "13.50",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/102.jpg",
"thumb_url": "https://cdn.alpha.example/102-s.jpg"
},
"why": "Set in the same universe: the politics of a desert moon instead of snow.",
"why_source": "model"
},
{
"product": {
"id": 104,
"title": "Solaris",
"maker": "Stanislaw Lem",
"line": null,
"url": "https://alpha.example/products/104",
"handle": "solaris",
"external_id": "gid://shop/104",
"price": {
"amount": "11.00",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/104.jpg",
"thumb_url": "https://cdn.alpha.example/104-s.jpg"
},
"why": "A different kind of stillness: an ocean planet that studies the people studying it.",
"why_source": "model"
}
],
"shelves": [],
"miss": false,
"miss_reason": null,
"clarify": null,
"degraded": false,
"currency": "EUR",
"ai": {
"generated": true,
"fields": [
"say",
"main.why",
"alternatives[].why"
],
"notice": "The `say` and `why` texts are generated by an AI model from this shop's catalogue and may contain mistakes. Product names, prices, stock and links come from the shop's catalogue, not from the model."
}
}Rate limit reached (HTTP 429)#
Exemple de requête, curl
curl -X POST 'https://assistant.onesite.agency/api/v1/answers' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"query": "a cold, quiet science fiction novel for a long winter"
}'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/answers", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"query": "a cold, quiet science fiction novel for a long winter"
}),
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.post(
"https://assistant.onesite.agency/api/v1/answers",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"query": "a cold, quiet science fiction novel for a long winter",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 429
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Rate limit reached: 20 calls per minute for this scope.",
"param": null,
"request_id": "req_9f2b000000000005"
}
}GET /suggestions#
Suggestions rapides de produits sous un champ de recherche pendant la frappe. Aucun modèle d'IA n'intervient.
Instant products for a half-typed query. No model is called and nothing is journalled, so it is cheap — but it is not a substitute for POST /answers when the shopper asked a full question.
- Scope requis
suggest- Limite
- 120 appels par 60 s
| Nom | Où | Type | Obligatoire | Contraintes | Description |
|---|---|---|---|---|---|
q | query | string | oui | longueur 1–500 | What has been typed so far. |
limit | query | integer | non | 1–20; par défaut 6 | |
in_stock_only | query | boolean | non | par défaut false | Only products that can be bought now. |
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | SuggestionList | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Suggestions while the shopper types (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/suggestions?q=left+hand&limit=3' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/suggestions?q=left+hand&limit=3", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/suggestions?q=left+hand&limit=3",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"object": "list",
"query": "left hand",
"data": [
{
"id": 101,
"title": "The Left Hand of Darkness",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/101",
"handle": "the-left-hand-of-darkness",
"external_id": "gid://shop/101",
"price": {
"amount": "12.90",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/101.jpg",
"thumb_url": "https://cdn.alpha.example/101-s.jpg"
}
]
}GET /products#
Lisez tout le catalogue page par page.
The shop's visible catalogue in a stable order (by product id), a page at a time. Products the owner has hidden or excluded are not listed. A page may hold fewer than limit items while has_more is still true; keep following next_cursor until it is null.
- Scope requis
catalogue:read- Limite
- 120 appels par 60 s
| Nom | Où | Type | Obligatoire | Contraintes | Description |
|---|---|---|---|---|---|
limit | query | integer | non | 1–100; par défaut 20 | |
cursor | query | string | null | non | longueur ≤ 64 | next_cursor of the previous page. |
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | ProductPage | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
First page of the catalogue (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/products?limit=2' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products?limit=2", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products?limit=2",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"object": "list",
"data": [
{
"id": 101,
"title": "The Left Hand of Darkness",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/101",
"handle": "the-left-hand-of-darkness",
"external_id": "gid://shop/101",
"price": {
"amount": "12.90",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/101.jpg",
"thumb_url": "https://cdn.alpha.example/101-s.jpg"
},
{
"id": 102,
"title": "The Dispossessed",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/102",
"handle": "the-dispossessed",
"external_id": "gid://shop/102",
"price": {
"amount": "13.50",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/102.jpg",
"thumb_url": "https://cdn.alpha.example/102-s.jpg"
}
],
"has_more": true,
"next_cursor": "c1.MTAy"
}Following the cursor (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/products?limit=2&cursor=c1.MTAy' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products?limit=2&cursor=c1.MTAy", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products?limit=2&cursor=c1.MTAy",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"object": "list",
"data": [
{
"id": 103,
"title": "A Wizard of Earthsea",
"maker": "Ursula K. Le Guin",
"line": "Earthsea",
"url": "https://alpha.example/products/103",
"handle": "a-wizard-of-earthsea",
"external_id": "gid://shop/103",
"price": {
"amount": "9.99",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/103.jpg",
"thumb_url": "https://cdn.alpha.example/103-s.jpg"
},
{
"id": 104,
"title": "Solaris",
"maker": "Stanislaw Lem",
"line": null,
"url": "https://alpha.example/products/104",
"handle": "solaris",
"external_id": "gid://shop/104",
"price": {
"amount": "11.00",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/104.jpg",
"thumb_url": "https://cdn.alpha.example/104-s.jpg"
}
],
"has_more": true,
"next_cursor": "c1.MTA0"
}A parameter out of range (HTTP 422)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/products?limit=500' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products?limit=500", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products?limit=500",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 422
{
"error": {
"type": "invalid_request_error",
"code": "invalid_request",
"message": "Invalid request. limit: Input should be less than or equal to 100",
"param": "limit",
"request_id": "req_9f2b000000000004"
}
}GET /products/{product_id}#
Lisez un produit par son identifiant.
One product of the shop with its description and gallery. A product that belongs to another shop, does not exist or is hidden by the owner is the same 404.
- Scope requis
catalogue:read- Limite
- 120 appels par 60 s
| Nom | Où | Type | Obligatoire | Contraintes | Description |
|---|---|---|---|---|---|
product_id | path | integer | oui | 1–9223372036854776000 | Product number (id in any product object). |
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | ProductDetail | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
404 | ErrorBody | No such visible product in this shop. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
One product with its description and gallery (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/products/101' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products/101", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products/101",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"id": 101,
"title": "The Left Hand of Darkness",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/101",
"handle": "the-left-hand-of-darkness",
"external_id": "gid://shop/101",
"price": {
"amount": "12.90",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/101.jpg",
"thumb_url": "https://cdn.alpha.example/101-s.jpg",
"description": "The Left Hand of Darkness by Ursula K. Le Guin.",
"images": [
"https://cdn.alpha.example/101.jpg"
]
}A key without the needed scope (HTTP 403)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/products/101' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products/101", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products/101",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 403
{
"error": {
"type": "permission_error",
"code": "insufficient_scope",
"message": "This key lacks the 'catalogue:read' scope. Create a key that has it.",
"param": "catalogue:read",
"request_id": "req_9f2b000000000002"
}
}A product that is not in this shop (HTTP 404)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/products/987654' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products/987654", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products/987654",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 404
{
"error": {
"type": "not_found_error",
"code": "product_not_found",
"message": "No such product in this shop.",
"param": null,
"request_id": "req_9f2b000000000003"
}
}GET /products/{product_id}/similar#
Produits similaires à un produit donné. Aucun modèle d'IA n'intervient.
Neighbours of the product's own embedding vector, ranked the way the shop's assistant ranks them. No model is called. Needs only the similar scope — a key without catalogue:read cannot fetch the product itself.
- Scope requis
similar- Limite
- 60 appels par 60 s
| Nom | Où | Type | Obligatoire | Contraintes | Description |
|---|---|---|---|---|---|
product_id | path | integer | oui | 1–9223372036854776000 | Product number (id in any product object). |
limit | query | integer | non | 1–24; par défaut 8 |
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | SimilarList | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
404 | ErrorBody | No such visible product in this shop. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Products similar to one product (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/products/101/similar?limit=3' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products/101/similar?limit=3", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products/101/similar?limit=3",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"object": "list",
"data": [
{
"id": 102,
"title": "The Dispossessed",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/102",
"handle": "the-dispossessed",
"external_id": "gid://shop/102",
"price": {
"amount": "13.50",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/102.jpg",
"thumb_url": "https://cdn.alpha.example/102-s.jpg"
},
{
"id": 103,
"title": "A Wizard of Earthsea",
"maker": "Ursula K. Le Guin",
"line": "Earthsea",
"url": "https://alpha.example/products/103",
"handle": "a-wizard-of-earthsea",
"external_id": "gid://shop/103",
"price": {
"amount": "9.99",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/103.jpg",
"thumb_url": "https://cdn.alpha.example/103-s.jpg"
},
{
"id": 109,
"title": "Gift Cookbook",
"maker": "Chef Example",
"line": null,
"url": "https://alpha.example/products/109",
"handle": "gift-cookbook",
"external_id": "gid://shop/109",
"price": {
"amount": "24.00",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/109.jpg",
"thumb_url": "https://cdn.alpha.example/109-s.jpg"
}
],
"product_id": 101
}GET /analytics/summary#
Les totaux de votre boutique sur une période — les mêmes chiffres que les Statistiques du cabinet —, avec une comparaison facultative.
The cabinet's Statistics figures for the window: questions, people who asked, sessions, found and not found, clicks, and the shopper's path by step. Counted for people only. With compare, the same totals for the comparison window — only when the shop's history covers it; otherwise previous is null rather than a growth from nothing.
- Scope requis
analytics:read- Limite
- 30 appels par 60 s
| Nom | Où | Type | Obligatoire | Contraintes | Description |
|---|---|---|---|---|---|
compare | query | "prev" | "year" | "none" | non | par défaut "prev" | Compare with the window just before (prev), the same window a year back (year), or nothing. |
since | query | string (date-time) | null | non | | Start of the window (ISO 8601; without an offset — UTC). Default: until minus 30 days. |
until | query | string (date-time) | null | non | | End of the window, exclusive (ISO 8601). Default: now. |
first_day | query | string (date) | null | non | | First day of the window (YYYY-MM-DD) in timezone, inclusive. Use with last_day instead of since/until. |
last_day | query | string (date) | null | non | | Last day of the window (YYYY-MM-DD) in timezone, inclusive. |
timezone | query | string | null | non | longueur ≤ 64 | IANA time zone for days, e.g. Europe/Berlin. Default: the shop's time zone, or UTC when the shop has none. |
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | AnalyticsSummary | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Last week in figures, compared with the week before (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/analytics/summary?first_day=2026-10-06&last_day=2026-10-12' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/analytics/summary?first_day=2026-10-06&last_day=2026-10-12", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/analytics/summary?first_day=2026-10-06&last_day=2026-10-12",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"object": "analytics.summary",
"window": {
"since": "2026-10-06T00:00:00+00:00",
"until": "2026-10-13T00:00:00+00:00",
"timezone": "UTC"
},
"totals": {
"queries": 280,
"people": 154,
"sessions": 174,
"answered": 238,
"missed": 42,
"asked_again": 31,
"impressions": 840,
"clicks": 82,
"reached": 63,
"browsed": 7
},
"compare": "prev",
"previous_window": {
"since": "2026-09-29T00:00:00+00:00",
"until": "2026-10-06T00:00:00+00:00"
},
"previous": {
"queries": 273,
"people": 150,
"sessions": 174,
"answered": 238,
"missed": 42,
"asked_again": 31,
"impressions": 840,
"clicks": 82,
"reached": 63,
"browsed": 7
},
"funnel": [
{
"stage": "visit",
"sessions": 900,
"share_of_previous": null
},
{
"stage": "offered",
"sessions": 860,
"share_of_previous": 0.9555555555555556
},
{
"stage": "opened",
"sessions": 140,
"share_of_previous": 0.16279069767441862
},
{
"stage": "asked",
"sessions": 120,
"share_of_previous": 0.8571428571428571
},
{
"stage": "reached",
"sessions": 30,
"share_of_previous": 0.25
},
{
"stage": "carted",
"sessions": 70,
"share_of_previous": null
},
{
"stage": "bought",
"sessions": 12,
"share_of_previous": 0.17142857142857143
}
],
"funnel_since": null
}GET /analytics/series#
Questions, questions sans résultat et questions ayant mené à un produit, pour chaque jour de la période.
Questions, misses and questions that reached a product, for every day of the window in timezone. Days without questions are zeros. Only the window is counted, so the first and last day may be partial, and the days add up to the summary's totals.
- Scope requis
analytics:read- Limite
- 30 appels par 60 s
| Nom | Où | Type | Obligatoire | Contraintes | Description |
|---|---|---|---|---|---|
since | query | string (date-time) | null | non | | Start of the window (ISO 8601; without an offset — UTC). Default: until minus 30 days. |
until | query | string (date-time) | null | non | | End of the window, exclusive (ISO 8601). Default: now. |
first_day | query | string (date) | null | non | | First day of the window (YYYY-MM-DD) in timezone, inclusive. Use with last_day instead of since/until. |
last_day | query | string (date) | null | non | | Last day of the window (YYYY-MM-DD) in timezone, inclusive. |
timezone | query | string | null | non | longueur ≤ 64 | IANA time zone for days, e.g. Europe/Berlin. Default: the shop's time zone, or UTC when the shop has none. |
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | AnalyticsSeries | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Questions per day (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/analytics/series?first_day=2026-10-06&last_day=2026-10-12&timezone=Europe/Berlin' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/analytics/series?first_day=2026-10-06&last_day=2026-10-12&timezone=Europe/Berlin", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/analytics/series?first_day=2026-10-06&last_day=2026-10-12&timezone=Europe/Berlin",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"object": "analytics.series",
"window": {
"since": "2026-10-05T22:00:00+00:00",
"until": "2026-10-12T22:00:00+00:00",
"timezone": "Europe/Berlin"
},
"data": [
{
"date": "2026-10-06",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-07",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-08",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-09",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-10",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-11",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-12",
"queries": 40,
"missed": 6,
"reached": 9
}
]
}GET /analytics/queries#
Les questions les plus posées et celles qui restent le plus souvent sans produit.
The questions people ask most, and the questions most often left without a product — what to stock and what to describe better. Questions are grouped without regard to case; e-mail addresses and phone or card numbers in them are masked.
- Scope requis
analytics:read- Limite
- 30 appels par 60 s
| Nom | Où | Type | Obligatoire | Contraintes | Description |
|---|---|---|---|---|---|
limit | query | integer | non | 1–100; par défaut 20 | Rows in each list. |
since | query | string (date-time) | null | non | | Start of the window (ISO 8601; without an offset — UTC). Default: until minus 30 days. |
until | query | string (date-time) | null | non | | End of the window, exclusive (ISO 8601). Default: now. |
first_day | query | string (date) | null | non | | First day of the window (YYYY-MM-DD) in timezone, inclusive. Use with last_day instead of since/until. |
last_day | query | string (date) | null | non | | Last day of the window (YYYY-MM-DD) in timezone, inclusive. |
timezone | query | string | null | non | longueur ≤ 64 | IANA time zone for days, e.g. Europe/Berlin. Default: the shop's time zone, or UTC when the shop has none. |
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | AnalyticsQueries | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
What people ask most, and what they do not find (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/analytics/queries?first_day=2026-10-06&last_day=2026-10-12&limit=3' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/analytics/queries?first_day=2026-10-06&last_day=2026-10-12&limit=3", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/analytics/queries?first_day=2026-10-06&last_day=2026-10-12&limit=3",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"object": "analytics.queries",
"window": {
"since": "2026-10-06T00:00:00+00:00",
"until": "2026-10-13T00:00:00+00:00",
"timezone": "UTC"
},
"top": [
{
"text": "science fiction for winter",
"count": 42,
"missed": 0,
"clicks": 28
},
{
"text": "gift for a ten-year-old",
"count": 28,
"missed": 7,
"clicks": 14
},
{
"text": "write to me at [email]",
"count": 1,
"missed": 1,
"clicks": 0
}
],
"missed": [
{
"text": "signed first editions",
"count": 21,
"missed": 21,
"clicks": 0
},
{
"text": "gift for a ten-year-old",
"count": 28,
"missed": 7,
"clicks": 14
}
]
}GET /analytics/sales#
Ce que l'assistant a vendu, d'après vos propres commandes — les mêmes chiffres que le résumé Achats du cabinet.
Orders and revenue the assistant helped with, by the shop's own orders: products its button put into the cart and products bought after it showed them. Orders after a conversation without either are counted apart and not as its revenue. The shop's total turnover is not part of this API.
- Scope requis
analytics:read- Limite
- 30 appels par 60 s
| Nom | Où | Type | Obligatoire | Contraintes | Description |
|---|---|---|---|---|---|
days | query | integer | non | par défaut 30 | Period: today and the days before it. One of 7, 30, 90. |
timezone | query | string | null | non | longueur ≤ 64 | IANA time zone for days. Default: the shop's time zone, or UTC. |
| Statut | Schéma de la réponse | Description |
|---|---|---|
200 | AnalyticsSales | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
What the assistant sold in the last 7 days (HTTP 200)#
Exemple de requête, curl
curl 'https://assistant.onesite.agency/api/v1/analytics/sales?days=7' \
-H 'Authorization: Bearer YOUR_API_KEY'Exemple de requête, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/analytics/sales?days=7", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Exemple de requête, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/analytics/sales?days=7",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Exemple de réponse · HTTP 200
{
"object": "analytics.sales",
"days": 7,
"since": "2026-10-07T00:00:00+00:00",
"timezone": "UTC",
"currency": "EUR",
"assistant_orders": 2,
"assistant_revenue": {
"amount": "23.90",
"currency": "EUR"
},
"button_revenue": {
"amount": "12.90",
"currency": "EUR"
},
"shown_revenue": {
"amount": "11.00",
"currency": "EUR"
},
"carted": 3,
"talked_orders": 1,
"average_order": {
"amount": "18.70",
"currency": "EUR"
},
"days_data": [
{
"date": "2026-10-07",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-08",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-09",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-10",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-11",
"orders": 1,
"revenue": {
"amount": "11.00",
"currency": "EUR"
}
},
{
"date": "2026-10-12",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-13",
"orders": 1,
"revenue": {
"amount": "12.90",
"currency": "EUR"
}
}
],
"products": [
{
"product_id": 101,
"title": "The Left Hand of Darkness",
"quantity": 1,
"revenue": {
"amount": "12.90",
"currency": "EUR"
}
},
{
"product_id": 104,
"title": "Solaris",
"quantity": 1,
"revenue": {
"amount": "11.00",
"currency": "EUR"
}
}
]
}Types de données#
Les objets utilisés dans les requêtes et les réponses. | null signifie que le champ est toujours présent mais peut valoir null ; un tableau s'écrit Type[].
AiDisclosure#
Shown to your users? Then say so. The wording below is the minimum we ask integrations to pass on.
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
generated | boolean | non | Some text in this response was written by an AI model. |
fields | string[] | oui | JSON paths of the AI-written strings. |
notice | string | oui |
AnalyticsFunnelStep#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
stage | "visit" | "offered" | "opened" | "asked" | "reached" | "carted" | "bought" | oui | |
sessions | integer | oui | Sessions that reached this step. Sessions, not people: events on the site carry no visitor mark. |
share_of_previous | number | null | non | sessions divided by the previous step. null on the first step and where a step is larger than the one before it (for example, carts added with the shop's own button without a click in an answer). |
AnalyticsPeriod#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
since | string | oui | |
until | string | oui |
AnalyticsPoint#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
date | string | oui | The day (YYYY-MM-DD) in window.timezone. The first and last day may be partial: only the window is counted. |
queries | integer | oui | |
missed | integer | oui | |
reached | integer | oui | Questions of the day after which the shopper opened a product page from the answer. |
AnalyticsQueries#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
object | "analytics.queries" | non | |
window | AnalyticsWindow | oui | |
top | AnalyticsQuery[] | oui | Most frequent questions, most frequent first. |
missed | AnalyticsQuery[] | oui | Questions most often left without a product, by number of misses. Its own list, not a filter of top. |
AnalyticsQuery#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
text | string | oui | The question as asked (case-insensitive group). E-mail addresses, phone and card numbers are masked. |
count | integer | oui | How many times it was asked. |
missed | integer | oui | How many of those had nothing to show. |
clicks | integer | oui | Clicks from its answers to product pages. |
AnalyticsSales#
What the assistant sold, by the shop's own orders — the same figures as the summary in the cabinet's Purchases section.
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
object | "analytics.sales" | non | |
days | integer | oui | Today and the days before it, from midnight in timezone. |
since | string | oui | Midnight of the first day (ISO 8601). |
timezone | string | oui | |
currency | string | oui | Orders in another currency are not added up. |
assistant_orders | integer | oui | Orders with a product added to the cart by the assistant's button or bought after the assistant showed it. |
assistant_revenue | Money | oui | The value of those products (not the whole orders). |
button_revenue | Money | oui | Part of assistant_revenue added to the cart by the assistant's button. |
shown_revenue | Money | oui | Part of assistant_revenue bought after the assistant showed the product. |
carted | integer | oui | Products the assistant helped to put into a cart in the period (by its button or after showing them). |
talked_orders | integer | oui | Orders placed after a conversation with the assistant without any of the above. Not counted as its revenue. |
average_order | Money | null | non | Average total of assistant_orders; null when none has a total. |
days_data | AnalyticsSalesDay[] | oui | Every day of the period, empty days included. |
products | AnalyticsSoldProduct[] | oui | Top products sold through the assistant, by revenue. |
AnalyticsSalesDay#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
date | string | oui | The day (YYYY-MM-DD) in timezone. |
orders | integer | oui | |
revenue | Money | oui |
AnalyticsSeries#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
object | "analytics.series" | non | |
window | AnalyticsWindow | oui | |
data | AnalyticsPoint[] | oui | Every day of the window, days without questions included as zeros. The sum of queries equals totals.queries of the summary. |
AnalyticsSoldProduct#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
product_id | integer | null | non | null when the product is no longer in the catalogue. |
title | string | null | non | |
quantity | integer | oui | |
revenue | Money | oui |
AnalyticsSummary#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
object | "analytics.summary" | non | |
window | AnalyticsWindow | oui | |
totals | AnalyticsTotals | oui | |
compare | "prev" | "year" | "none" | oui | |
previous_window | AnalyticsPeriod | null | non | The window compared with. null when compare is none or the shop's history covers less than 90 % of it. |
previous | AnalyticsTotals | null | non | Totals of previous_window, counted the same way. null — no comparison. |
funnel | AnalyticsFunnelStep[] | oui | Sessions by step of the shopper's path, in order. |
funnel_since | string | null | non | Set when the funnel is counted from a later moment than window.since: before it one of the steps was not yet recorded. |
AnalyticsTotals#
Counted for people only: bots, our own checks, calls made with API keys and conversations removed by the operator are not counted.
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
queries | integer | oui | Questions people asked the assistant, in their own words. |
people | integer | oui | Different people who asked at least once, by the visitor mark in their browser: someone back three days later is one person, the same person on phone and laptop is two. A question without the mark (no analytics consent) counts its session. |
sessions | integer | oui | Different sessions with at least one question. A session lasts about a day, so this is not people. |
answered | integer | oui | Questions answered with at least one product. |
missed | integer | oui | Questions with nothing to show. |
asked_again | integer | oui | Sessions that asked another question after an answer. |
impressions | integer | oui | Product cards shown in answers. |
clicks | integer | oui | Clicks from answer cards to product pages. |
reached | integer | oui | Questions after which the shopper opened a product page from the answer. Counted per question, not per click. |
browsed | integer | oui | Clicks to product pages from shelves and collections, with no question before them. |
AnalyticsWindow#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
since | string | oui | Start of the window, inclusive (ISO 8601, UTC). |
until | string | oui | End of the window, exclusive (ISO 8601, UTC). |
timezone | string | oui | IANA time zone in which days are cut (daily series, first_day/last_day). |
Answer#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
id | string | oui | Id of this answer in the shop's journal. Quote it when you report a problem. |
query | string | oui | |
intent | string | oui | What the question was understood to be: search, browse, basket, question, greeting, off_topic, code, about_item, order…. New values may appear; treat unknown ones as search. |
say | string | null | non | A short spoken-style reply, written by an AI model. May be null. |
main | Recommendation | null | non | The single best match, when there is one. |
alternatives | Recommendation[] | non | |
shelves | Shelf[] | non | For “everything by …” questions: whole shelves instead of picks. |
miss | boolean | oui | true when the shop has nothing that fits. Do not invent a product then. |
miss_reason | "absent" | "unstocked" | "unmatched" | null | non | Why nothing is shown: not in the catalogue (absent), sold out (unstocked), or nothing matched the question's hard limits such as price (unmatched). |
clarify | string | null | non | A clarifying question to ask the user, when the answer needs one. |
degraded | boolean | non | true when the answer was built without the model (it was unavailable). |
currency | string | oui | |
ai | AiDisclosure | oui |
AnswerRequest#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
query | string | oui | The shopper's question, in their own words. |
language | Language | null | non | Language of the reply (ru en el es de it fr). Defaults to the shop's language. |
session_id | string | null | non | Opaque id of one conversation. Send the same id to let follow-ups (“cheaper”, “something lighter”) build on earlier turns. Sessions are private to your API key. |
in_stock_only | boolean | non | Only products that can be bought now (excludes pre-orders). |
ErrorBody#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
error | ErrorDetail | oui |
ErrorDetail#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
type | "authentication_error" | "permission_error" | "invalid_request_error" | "not_found_error" | "conflict_error" | "rate_limit_error" | "billing_error" | "api_error" | oui | |
code | string | oui | Stable machine-readable reason. Branch on this, not on message. |
message | string | oui | For humans. The wording may change between releases. |
param | string | null | non | The offending field or parameter, for invalid_request. |
request_id | string | oui | Also in the X-Request-Id response header. |
KeyInfo#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
id | integer | oui | |
name | string | oui | |
hint | string | oui | Which key this is, e.g. osk_…x9Zq. The secret itself is never shown again. |
scopes | string[] | oui | |
shop | KeyShop | oui | |
origins | string[] | oui | Browser origins allowed to use this key. Empty = any (server-side use). |
networks | string[] | oui | IP networks the key is accepted from. Empty = any. |
created_at | string | oui | |
last_used_at | string | null | non | |
limits | map<string, ScopeLimit> | oui | Calls per minute, per scope. |
answers_per_day | integer | oui | Daily cap on POST /answers calls for this key (UTC day). |
KeyShop#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
domain | string | oui | |
currency | string | oui | |
language | string | oui |
Language#
Language code: ru, en, el, es, de, it, fr.
"ru" | "en" | "el" | "es" | "de" | "it" | "fr"
Money#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
amount | string | oui | Decimal amount as a string, e.g. "12.90". Never a float: no rounding surprises. |
currency | string | oui | ISO 4217 code of the shop currency, e.g. EUR. |
Product#
A catalogue card. Everything here comes from the shop's own catalogue, never from the model.
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
id | integer | oui | Product number in this shop's catalogue. Stable until the product is removed from the shop. |
title | string | oui | |
maker | string | null | non | Whoever the shop credits as the maker of the product. |
line | string | null | non | Series, collection or product line, when the shop names one. |
url | string | null | non | Product page on the shop's own site. |
handle | string | null | non | The platform's product handle (Shopify handle, WooCommerce slug), when known. |
external_id | string | null | non | The shop platform's own product id, for mapping back to your system. |
price | Money | null | non | Current price; null when the shop does not publish one. |
availability | "in_stock" | "preorder" | "out_of_stock" | oui | |
ready_at | string | null | non | For preorder: the shop's promised ship date (ISO 8601). |
image_url | string | null | non | |
thumb_url | string | null | non | A smaller image, when the shop has one. |
ProductDetail#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
id | integer | oui | Product number in this shop's catalogue. Stable until the product is removed from the shop. |
title | string | oui | |
maker | string | null | non | Whoever the shop credits as the maker of the product. |
line | string | null | non | Series, collection or product line, when the shop names one. |
url | string | null | non | Product page on the shop's own site. |
handle | string | null | non | The platform's product handle (Shopify handle, WooCommerce slug), when known. |
external_id | string | null | non | The shop platform's own product id, for mapping back to your system. |
price | Money | null | non | Current price; null when the shop does not publish one. |
availability | "in_stock" | "preorder" | "out_of_stock" | oui | |
ready_at | string | null | non | For preorder: the shop's promised ship date (ISO 8601). |
image_url | string | null | non | |
thumb_url | string | null | non | A smaller image, when the shop has one. |
description | string | null | non | |
images | string[] | non | The shop's gallery, in the shop's order. |
ProductPage#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
object | "list" | non | |
data | Product[] | oui | |
has_more | boolean | oui | |
next_cursor | string | null | non | Pass as cursor to get the next page. null on the last page. |
Recommendation#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
product | Product | oui | |
why | string | null | non | One sentence on why this fits the question. Written by an AI model when why_source is model; a fixed catalogue template when it is catalogue. |
why_source | "model" | "catalogue" | null | non | Who wrote why. null when there is no why. |
ScopeLimit#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
per_minute | integer | oui |
Shelf#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
title | string | oui | |
total | integer | oui | How many products the shelf holds in the shop — not how many are shown in items. |
items | Product[] | oui |
SimilarList#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
object | "list" | non | |
data | Product[] | oui | |
product_id | integer | oui |
SuggestionList#
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
object | "list" | non | |
query | string | oui | |
data | Product[] | oui |
Erreurs#
Toutes les erreurs ont la même forme et un request_id (aussi dans l'en-tête X-Request-Id) : citez-le quand vous nous écrivez. Branchez-vous sur code, jamais sur message, dont le texte peut changer.
Erreurs · HTTP 422
{
"error": {
"type": "invalid_request_error",
"code": "invalid_request",
"message": "Invalid request. limit: Input should be less than or equal to 100",
"param": "limit",
"request_id": "req_9f2b000000000004"
}
}| Code | HTTP | Ce que cela signifie et que faire | Nouvel essai |
|---|---|---|---|
invalid_api_key | 401 | La clé est absente, mal formée, inconnue ou révoquée. Les quatre cas se présentent de la même façon, volontairement. Envoyez Authorization: Bearer <clé> ou créez une nouvelle clé. | corrigez la requête |
ip_not_allowed | 403 | La clé a une liste d'IP autorisées et cette adresse n'en fait pas partie. Appelez depuis un réseau de la liste ou modifiez la liste dans le cabinet. | corrigez la requête |
origin_not_allowed | 403 | L'appel vient d'une page de navigateur dont l'origine ne figure pas dans la liste d'origines de la clé. Ajoutez l'origine exacte (schéma, hôte, port) dans le cabinet. | corrigez la requête |
insufficient_scope | 403 | La clé n'a pas la portée (scope) requise par ce endpoint. Les scopes ne s'élargissent pas : créez une nouvelle clé avec ce scope. | corrigez la requête |
api_disabled | 403 | L'API développeur n'est pas activée pour cette boutique. Ouvrez l'onglet Developers du cabinet, ou écrivez-nous si vous ne la voyez pas. | corrigez la requête |
subscription_inactive | 402 | L'abonnement de la boutique n'est pas actif, l'API est donc en pause. Elle reprend d'elle-même dès que l'abonnement est de nouveau actif ; les clés restent valides. | corrigez la requête |
plan_not_entitled | 403 | Le forfait de la boutique n'inclut pas l'API développeur. Elle est disponible sur les forfaits qui l'incluent. | corrigez la requête |
rate_limited | 429 | La limite par minute de cette clé et de ce scope est atteinte. Attendez les secondes de Retry-After puis réessayez ; espacez les appels ou mettez les résultats en cache. | plus tard |
daily_quota_exceeded | 429 | Le plafond quotidien de réponses de cette clé est atteint (jour UTC). Réessayez après les secondes de Retry-After, c'est-à-dire au changement de jour. | plus tard |
too_many_failed_attempts | 429 | Trop de clés invalides sont venues de cette adresse. Attendez les secondes de Retry-After et vérifiez quelle clé envoie votre code. | plus tard |
invalid_request | 422 | Un paramètre ou un champ du corps est absent ou hors limites. param désigne le premier fautif ; corrigez la requête, réessayer n'y changera rien. | corrigez la requête |
invalid_cursor | 422 | Le curseur a été modifié ou n'appartient pas à cette liste. Transmettez next_cursor tel que reçu, ou repartez sans curseur. | corrigez la requête |
product_not_found | 404 | Ce produit n'existe pas dans cette boutique. L'identifiant d'un produit d'une autre boutique donne la même réponse : une clé ne lit que sa propre boutique. | corrigez la requête |
idempotency_key_reused | 422 | Ce Idempotency-Key a déjà servi avec un corps différent. Utilisez une nouvelle clé pour une autre question. | corrigez la requête |
idempotency_in_progress | 409 | Une requête avec le même Idempotency-Key est encore en cours de traitement. Patientez un instant et renvoyez la même requête pour obtenir son résultat. | plus tard |
request_too_large | 413 | Le corps de la requête dépasse la limite. Envoyez un corps plus petit. | corrigez la requête |
answer_unavailable | 502 | La réponse n'a pas pu être produite pour le moment. Réessayez bientôt avec le même Idempotency-Key . | plus tard |
internal_error | 500 | Un problème est survenu de notre côté. Réessayez après une pause ; s'il persiste, envoyez-nous le request_id. | plus tard |
not_found | 404 | Ce endpoint n'existe pas. Vérifiez le chemin dans la référence ; les chemins commencent par la version, /api/v1. | corrigez la requête |
method_not_allowed | 405 | Le chemin existe, mais pas pour cette méthode HTTP. L'en-tête Allow liste celles qui sont permises. | corrigez la requête |
type regroupe les codes : authentication_error, permission_error, invalid_request_error, not_found_error, conflict_error, rate_limit_error, billing_error, api_error. De nouveaux codes peuvent apparaître dans v1 ; traitez un code inconnu d'après son statut HTTP.
Pagination#
Les listes utilisent des curseurs. Passez next_cursor d'une réponse comme cursor dans la requête suivante jusqu'à ce que next_cursor soit null. limit vaut 20 par défaut et au plus 100. Une page peut contenir moins d'éléments que limit tant que has_more est true : suivez next_cursor au lieu de compter. Considérez le curseur comme opaque : un curseur altéré donne 422 invalid_cursor.
Cache et ETag#
Les réponses GET portent un ETag. Renvoyez-le en If-None-Match ; si rien n'a changé, vous recevez 304 Not Modified sans corps, plus rapide et plus léger que de retélécharger la liste. Mettez en cache les lectures du catalogue et revalidez ainsi.
Idempotence et nouvelles tentatives#
POST /answers accepte l'en-tête Idempotency-Key (de 1 à 64 caractères : lettres, chiffres et . _ : -). Répéter le même corps dans les 24 heures renvoie la première réponse sans second appel au modèle et sans consommer de quota ; le rejeu porte Idempotent-Replayed: true. La même clé avec un autre corps donne 422 idempotency_key_reused ; tant que la première requête tourne, 409 idempotency_in_progress.
Créez un nouvel UUID pour chaque question de l'utilisateur et ne le réutilisez que pour les nouvelles tentatives de cette même question.
Les corps de requête sont limités à 16384 octets (413 request_too_large).
Navigateurs et CORS#
Une page web ne peut appeler l'API que depuis une origine listée sur la clé (schéma, hôte et port, par exemple https://shop.example) ; l'API n'envoie les en-têtes CORS qu'aux origines listées. Tout ce qui figure dans le code d'une page est public : une clé qui s'y trouve peut être copiée par quiconque ouvre la page.
Pour un usage dans le navigateur, ne donnez donc à la clé que les scopes suggest et similar avec une liste d'origines, et gardez les clés answer sur votre serveur.
Analytique#
Le droit analytics:read ouvre les chiffres de votre propre boutique : les mêmes totaux, série quotidienne, questions principales et ventes de l'assistant que le cabinet, calculés par le même code. Ne le donnez qu'aux clés qui restent sur votre serveur.
Seules les personnes sont comptées : ni les robots, ni nos propres vérifications, ni les appels faits avec des clés d'API, ni les conversations retirées par l'opérateur. people compte les visiteurs distincts par la marque de leur navigateur — quelqu'un qui revient trois jours plus tard est une personne, la même personne sur mobile et portable en fait deux ; les sessions durent environ un jour ; les étapes de l'entonnoir sont des sessions. Le chiffre d'affaires total de la boutique ne fait pas partie de l'API.
Une période est soit since/until (ISO 8601 ; sans décalage, UTC), soit first_day/last_day, les deux jours inclus, découpés dans timezone (par défaut, le fuseau de la boutique, ou UTC). Une comparaison (compare) n'est renvoyée que si l'historique de la boutique couvre la période de comparaison ; sinon previous vaut null.
Texte généré par IA#
Dans une réponse, say et chaque why sont écrits par un modèle d'IA, et la réponse le dit dans son objet ai : generated, les chemins JSON dans fields et un texte notice. Si vous montrez ces textes à vos utilisateurs, dites-leur qu'ils sont générés par IA ; notice est la formulation minimale que nous demandons aux intégrations de transmettre.
Noms, prix, stocks et liens ne viennent jamais du modèle. Quand miss vaut true, la boutique n'a rien qui convienne : montrez-le et n'inventez pas de produit.
Versions#
La version fait partie du chemin : /v1. Dans v1, nous ne faisons que des changements rétrocompatibles : nouveaux endpoints, nouveaux paramètres optionnels, nouveaux champs de réponse et nouvelles valeurs d'énumérations (par exemple de nouvelles valeurs d'intent). Écrivez des clients qui ignorent les champs inconnus et se replient raisonnablement sur les valeurs d'énumération inconnues. Un changement incompatible ne serait publié que sous un nouveau chemin de version.
Pour les machines et assistants IA#
La même référence sous forme lisible par machine. Importez le document OpenAPI dans Postman, Insomnia ou un générateur de code.