API para desarrolladores
Una API de solo lectura al asistente de tu propia tienda: respuestas, sugerencias mientras se escribe, productos similares y catálogo, para tu servidor, tu app o tu página.
Versión de la API 1.0.0
Resumen#
La API da a tu código lo que el asistente del sitio da a tus compradores: una respuesta completa a una pregunta en lenguaje natural, sugerencias de productos mientras se escribe, listas de productos similares y lectura del catálogo. URL base: https://assistant.onesite.agency/api/v1.
Es de solo lectura y limitada a una tienda: una clave abre la tienda que la creó y nada más. Nombres, precios, existencias y enlaces vienen siempre del catálogo; la IA solo escribe los textos cortos say y why, y la respuesta los marca (véase «Texto generado por IA» más abajo).
Las peticiones y respuestas son JSON sobre HTTPS. La versión va en la ruta (/v1). La API está disponible en los planes que incluyen la API para desarrolladores.
Inicio rápido#
De cero a la primera respuesta en tres pasos.
1. Crea una clave#
En el panel abre la pestaña Developers, pon nombre a la clave y elige sus scopes; opcionalmente, una lista de IP y orígenes de navegador. La clave se muestra una sola vez y solo guardamos un hash: cópiala directamente a los secretos de tu servidor, nunca a un repositorio. Una clave perdida no se puede recuperar: revócala y crea otra.
2. Compruébala#
Llama a GET /me. Un 200 muestra qué tienda abre la clave, sus scopes y sus límites. Sustituye YOUR_API_KEY por tu clave.
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/me' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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. Haz una pregunta#
Envía las palabras del comprador a POST /answers (requiere el scope answer). Recibes la mejor coincidencia, alternativas y una razón de una línea para cada una.
Ejemplo de petición, 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"
}'Ejemplo de petición, 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);Ejemplo de petición, 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())Autenticación#
Envía la clave en la cabecera Authorization como token bearer. Nunca la pongas en la URL: las URL acaban en registros y en el historial del navegador.
Autenticación
Authorization: Bearer YOUR_API_KEYUna clave ausente, mal formada, desconocida o revocada da siempre el mismo 401 invalid_api_key, de modo que la respuesta nunca le dice a quien adivina que una clave casi acierta. Muchas claves inválidas desde una dirección se frenan con 429 too_many_failed_attempts.
Una clave pertenece a una sola tienda y no puede leer otra: el id de un producto de otra tienda responde 404, igual que uno inexistente.
Puedes acotar más una clave con una lista de IP permitidas (403 ip_not_allowed) y una lista de orígenes de navegador (403 origin_not_allowed). Los scopes se fijan al crear la clave; para ampliarlos, crea otra clave.
Scopes y límites#
Cada clave lleva los scopes que puede usar. Los límites se aplican por clave y por scope.
| Scope | Endpoints | Llamadas por 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 tiene además un tope diario de 500 llamadas por clave (día UTC), porque cada respuesta es una llamada al modelo. Una repetición con el mismo Idempotency-Key no cuenta.
Cada respuesta incluye X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset; un 429 añade Retry-After (segundos). Espera ese tiempo en lugar de reintentar en bucle. GET /me requiere una clave válida, pero ningún scope concreto.
Endpoints#
Cada operación de abajo se construye desde el documento OpenAPI y cada ejemplo se ejecuta contra la API real en nuestras pruebas, así que los ejemplos no pueden desviarse de la API. Las descripciones de operaciones y campos están en inglés, como en el documento.
GET /me#
Comprueba qué tienda, scopes y límites tiene una clave.
Describes the key you are calling with: its scopes, shop, allow-lists and limits. Costs no quota; use it to check a key.
- Scope requerido
- cualquier clave válida
Sin parámetros.
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/me' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/me'Ejemplo de petición, 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);Ejemplo de petición, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/me",
)
print(response.status_code, response.json())Ejemplo de respuesta · 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#
Haz una pregunta con las palabras del comprador y recibe la mejor coincidencia, alternativas y una razón para cada una.
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 requerido
answer- Límite
- 20 llamadas por 60 s
| Nombre | Dónde | Tipo | Obligatorio | Restricciones | Descripción |
|---|---|---|---|---|---|
Idempotency-Key | header | string | null | no | longitud 1–64 | Makes a retry safe: the same key and body return the first answer without a second model call. |
Cuerpo de la petición: AnswerRequest#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
query | string | sí | The shopper's question, in their own words. |
language | Language | null | no | Language of the reply (ru en el es de it fr). Defaults to the shop's language. |
session_id | string | null | no | 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 | no | Only products that can be bought now (excludes pre-orders). |
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, 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"
}'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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)#
Ejemplo de petición, 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"
}'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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#
Sugerencias rápidas de productos bajo un cuadro de búsqueda mientras se escribe. No interviene ningún modelo de IA.
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 requerido
suggest- Límite
- 120 llamadas por 60 s
| Nombre | Dónde | Tipo | Obligatorio | Restricciones | Descripción |
|---|---|---|---|---|---|
q | query | string | sí | longitud 1–500 | What has been typed so far. |
limit | query | integer | no | 1–20; por defecto 6 | |
in_stock_only | query | boolean | no | por defecto false | Only products that can be bought now. |
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/suggestions?q=left+hand&limit=3' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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#
Lee todo el catálogo página a página.
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 requerido
catalogue:read- Límite
- 120 llamadas por 60 s
| Nombre | Dónde | Tipo | Obligatorio | Restricciones | Descripción |
|---|---|---|---|---|---|
limit | query | integer | no | 1–100; por defecto 20 | |
cursor | query | string | null | no | longitud ≤ 64 | next_cursor of the previous page. |
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/products?limit=2' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/products?limit=2&cursor=c1.MTAy' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/products?limit=500' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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}#
Lee un producto por su id.
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 requerido
catalogue:read- Límite
- 120 llamadas por 60 s
| Nombre | Dónde | Tipo | Obligatorio | Restricciones | Descripción |
|---|---|---|---|---|---|
product_id | path | integer | sí | 1–9223372036854776000 | Product number (id in any product object). |
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/products/101' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/products/101' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/products/987654' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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#
Productos similares a uno dado. No interviene ningún modelo de IA.
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 requerido
similar- Límite
- 60 llamadas por 60 s
| Nombre | Dónde | Tipo | Obligatorio | Restricciones | Descripción |
|---|---|---|---|---|---|
product_id | path | integer | sí | 1–9223372036854776000 | Product number (id in any product object). |
limit | query | integer | no | 1–24; por defecto 8 |
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/products/101/similar?limit=3' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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#
Los totales de tu tienda en un periodo —las mismas cifras que la Estadística del panel—, con comparación opcional.
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 requerido
analytics:read- Límite
- 30 llamadas por 60 s
| Nombre | Dónde | Tipo | Obligatorio | Restricciones | Descripción |
|---|---|---|---|---|---|
compare | query | "prev" | "year" | "none" | no | por defecto "prev" | Compare with the window just before (prev), the same window a year back (year), or nothing. |
since | query | string (date-time) | null | no | | Start of the window (ISO 8601; without an offset — UTC). Default: until minus 30 days. |
until | query | string (date-time) | null | no | | End of the window, exclusive (ISO 8601). Default: now. |
first_day | query | string (date) | null | no | | 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 | no | | Last day of the window (YYYY-MM-DD) in timezone, inclusive. |
timezone | query | string | null | no | longitud ≤ 64 | IANA time zone for days, e.g. Europe/Berlin. Default: the shop's time zone, or UTC when the shop has none. |
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, 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'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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#
Preguntas, preguntas sin resultado y preguntas que llegaron a un producto, para cada día del periodo.
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 requerido
analytics:read- Límite
- 30 llamadas por 60 s
| Nombre | Dónde | Tipo | Obligatorio | Restricciones | Descripción |
|---|---|---|---|---|---|
since | query | string (date-time) | null | no | | Start of the window (ISO 8601; without an offset — UTC). Default: until minus 30 days. |
until | query | string (date-time) | null | no | | End of the window, exclusive (ISO 8601). Default: now. |
first_day | query | string (date) | null | no | | 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 | no | | Last day of the window (YYYY-MM-DD) in timezone, inclusive. |
timezone | query | string | null | no | longitud ≤ 64 | IANA time zone for days, e.g. Europe/Berlin. Default: the shop's time zone, or UTC when the shop has none. |
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, 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'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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#
Las preguntas más frecuentes y las que más a menudo se quedan sin producto.
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 requerido
analytics:read- Límite
- 30 llamadas por 60 s
| Nombre | Dónde | Tipo | Obligatorio | Restricciones | Descripción |
|---|---|---|---|---|---|
limit | query | integer | no | 1–100; por defecto 20 | Rows in each list. |
since | query | string (date-time) | null | no | | Start of the window (ISO 8601; without an offset — UTC). Default: until minus 30 days. |
until | query | string (date-time) | null | no | | End of the window, exclusive (ISO 8601). Default: now. |
first_day | query | string (date) | null | no | | 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 | no | | Last day of the window (YYYY-MM-DD) in timezone, inclusive. |
timezone | query | string | null | no | longitud ≤ 64 | IANA time zone for days, e.g. Europe/Berlin. Default: the shop's time zone, or UTC when the shop has none. |
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, 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'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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#
Lo que vendió el asistente según tus propios pedidos: las mismas cifras que el resumen de Compras del panel.
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 requerido
analytics:read- Límite
- 30 llamadas por 60 s
| Nombre | Dónde | Tipo | Obligatorio | Restricciones | Descripción |
|---|---|---|---|---|---|
days | query | integer | no | por defecto 30 | Period: today and the days before it. One of 7, 30, 90. |
timezone | query | string | null | no | longitud ≤ 64 | IANA time zone for days. Default: the shop's time zone, or UTC. |
| Estado | Esquema de la respuesta | Descripción |
|---|---|---|
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)#
Ejemplo de petición, curl
curl 'https://assistant.onesite.agency/api/v1/analytics/sales?days=7' \
-H 'Authorization: Bearer YOUR_API_KEY'Ejemplo de petición, 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);Ejemplo de petición, 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())Ejemplo de respuesta · 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"
}
}
]
}Tipos de datos#
Los objetos usados en peticiones y respuestas. | null significa que el campo siempre está presente pero puede ser null; un array se escribe Tipo[].
AiDisclosure#
Shown to your users? Then say so. The wording below is the minimum we ask integrations to pass on.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
generated | boolean | no | Some text in this response was written by an AI model. |
fields | string[] | sí | JSON paths of the AI-written strings. |
notice | string | sí |
AnalyticsFunnelStep#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
stage | "visit" | "offered" | "opened" | "asked" | "reached" | "carted" | "bought" | sí | |
sessions | integer | sí | Sessions that reached this step. Sessions, not people: events on the site carry no visitor mark. |
share_of_previous | number | null | no | 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#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
since | string | sí | |
until | string | sí |
AnalyticsPoint#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
date | string | sí | The day (YYYY-MM-DD) in window.timezone. The first and last day may be partial: only the window is counted. |
queries | integer | sí | |
missed | integer | sí | |
reached | integer | sí | Questions of the day after which the shopper opened a product page from the answer. |
AnalyticsQueries#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
object | "analytics.queries" | no | |
window | AnalyticsWindow | sí | |
top | AnalyticsQuery[] | sí | Most frequent questions, most frequent first. |
missed | AnalyticsQuery[] | sí | Questions most often left without a product, by number of misses. Its own list, not a filter of top. |
AnalyticsQuery#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
text | string | sí | The question as asked (case-insensitive group). E-mail addresses, phone and card numbers are masked. |
count | integer | sí | How many times it was asked. |
missed | integer | sí | How many of those had nothing to show. |
clicks | integer | sí | 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.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
object | "analytics.sales" | no | |
days | integer | sí | Today and the days before it, from midnight in timezone. |
since | string | sí | Midnight of the first day (ISO 8601). |
timezone | string | sí | |
currency | string | sí | Orders in another currency are not added up. |
assistant_orders | integer | sí | Orders with a product added to the cart by the assistant's button or bought after the assistant showed it. |
assistant_revenue | Money | sí | The value of those products (not the whole orders). |
button_revenue | Money | sí | Part of assistant_revenue added to the cart by the assistant's button. |
shown_revenue | Money | sí | Part of assistant_revenue bought after the assistant showed the product. |
carted | integer | sí | Products the assistant helped to put into a cart in the period (by its button or after showing them). |
talked_orders | integer | sí | Orders placed after a conversation with the assistant without any of the above. Not counted as its revenue. |
average_order | Money | null | no | Average total of assistant_orders; null when none has a total. |
days_data | AnalyticsSalesDay[] | sí | Every day of the period, empty days included. |
products | AnalyticsSoldProduct[] | sí | Top products sold through the assistant, by revenue. |
AnalyticsSalesDay#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
date | string | sí | The day (YYYY-MM-DD) in timezone. |
orders | integer | sí | |
revenue | Money | sí |
AnalyticsSeries#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
object | "analytics.series" | no | |
window | AnalyticsWindow | sí | |
data | AnalyticsPoint[] | sí | Every day of the window, days without questions included as zeros. The sum of queries equals totals.queries of the summary. |
AnalyticsSoldProduct#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
product_id | integer | null | no | null when the product is no longer in the catalogue. |
title | string | null | no | |
quantity | integer | sí | |
revenue | Money | sí |
AnalyticsSummary#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
object | "analytics.summary" | no | |
window | AnalyticsWindow | sí | |
totals | AnalyticsTotals | sí | |
compare | "prev" | "year" | "none" | sí | |
previous_window | AnalyticsPeriod | null | no | The window compared with. null when compare is none or the shop's history covers less than 90 % of it. |
previous | AnalyticsTotals | null | no | Totals of previous_window, counted the same way. null — no comparison. |
funnel | AnalyticsFunnelStep[] | sí | Sessions by step of the shopper's path, in order. |
funnel_since | string | null | no | 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.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
queries | integer | sí | Questions people asked the assistant, in their own words. |
people | integer | sí | 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 | sí | Different sessions with at least one question. A session lasts about a day, so this is not people. |
answered | integer | sí | Questions answered with at least one product. |
missed | integer | sí | Questions with nothing to show. |
asked_again | integer | sí | Sessions that asked another question after an answer. |
impressions | integer | sí | Product cards shown in answers. |
clicks | integer | sí | Clicks from answer cards to product pages. |
reached | integer | sí | Questions after which the shopper opened a product page from the answer. Counted per question, not per click. |
browsed | integer | sí | Clicks to product pages from shelves and collections, with no question before them. |
AnalyticsWindow#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
since | string | sí | Start of the window, inclusive (ISO 8601, UTC). |
until | string | sí | End of the window, exclusive (ISO 8601, UTC). |
timezone | string | sí | IANA time zone in which days are cut (daily series, first_day/last_day). |
Answer#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string | sí | Id of this answer in the shop's journal. Quote it when you report a problem. |
query | string | sí | |
intent | string | sí | 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 | no | A short spoken-style reply, written by an AI model. May be null. |
main | Recommendation | null | no | The single best match, when there is one. |
alternatives | Recommendation[] | no | |
shelves | Shelf[] | no | For “everything by …” questions: whole shelves instead of picks. |
miss | boolean | sí | true when the shop has nothing that fits. Do not invent a product then. |
miss_reason | "absent" | "unstocked" | "unmatched" | null | no | 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 | no | A clarifying question to ask the user, when the answer needs one. |
degraded | boolean | no | true when the answer was built without the model (it was unavailable). |
currency | string | sí | |
ai | AiDisclosure | sí |
AnswerRequest#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
query | string | sí | The shopper's question, in their own words. |
language | Language | null | no | Language of the reply (ru en el es de it fr). Defaults to the shop's language. |
session_id | string | null | no | 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 | no | Only products that can be bought now (excludes pre-orders). |
ErrorBody#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
error | ErrorDetail | sí |
ErrorDetail#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
type | "authentication_error" | "permission_error" | "invalid_request_error" | "not_found_error" | "conflict_error" | "rate_limit_error" | "billing_error" | "api_error" | sí | |
code | string | sí | Stable machine-readable reason. Branch on this, not on message. |
message | string | sí | For humans. The wording may change between releases. |
param | string | null | no | The offending field or parameter, for invalid_request. |
request_id | string | sí | Also in the X-Request-Id response header. |
KeyInfo#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | integer | sí | |
name | string | sí | |
hint | string | sí | Which key this is, e.g. osk_…x9Zq. The secret itself is never shown again. |
scopes | string[] | sí | |
shop | KeyShop | sí | |
origins | string[] | sí | Browser origins allowed to use this key. Empty = any (server-side use). |
networks | string[] | sí | IP networks the key is accepted from. Empty = any. |
created_at | string | sí | |
last_used_at | string | null | no | |
limits | map<string, ScopeLimit> | sí | Calls per minute, per scope. |
answers_per_day | integer | sí | Daily cap on POST /answers calls for this key (UTC day). |
KeyShop#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
domain | string | sí | |
currency | string | sí | |
language | string | sí |
Language#
Language code: ru, en, el, es, de, it, fr.
"ru" | "en" | "el" | "es" | "de" | "it" | "fr"
Money#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
amount | string | sí | Decimal amount as a string, e.g. "12.90". Never a float: no rounding surprises. |
currency | string | sí | 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.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | integer | sí | Product number in this shop's catalogue. Stable until the product is removed from the shop. |
title | string | sí | |
maker | string | null | no | Whoever the shop credits as the maker of the product. |
line | string | null | no | Series, collection or product line, when the shop names one. |
url | string | null | no | Product page on the shop's own site. |
handle | string | null | no | The platform's product handle (Shopify handle, WooCommerce slug), when known. |
external_id | string | null | no | The shop platform's own product id, for mapping back to your system. |
price | Money | null | no | Current price; null when the shop does not publish one. |
availability | "in_stock" | "preorder" | "out_of_stock" | sí | |
ready_at | string | null | no | For preorder: the shop's promised ship date (ISO 8601). |
image_url | string | null | no | |
thumb_url | string | null | no | A smaller image, when the shop has one. |
ProductDetail#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | integer | sí | Product number in this shop's catalogue. Stable until the product is removed from the shop. |
title | string | sí | |
maker | string | null | no | Whoever the shop credits as the maker of the product. |
line | string | null | no | Series, collection or product line, when the shop names one. |
url | string | null | no | Product page on the shop's own site. |
handle | string | null | no | The platform's product handle (Shopify handle, WooCommerce slug), when known. |
external_id | string | null | no | The shop platform's own product id, for mapping back to your system. |
price | Money | null | no | Current price; null when the shop does not publish one. |
availability | "in_stock" | "preorder" | "out_of_stock" | sí | |
ready_at | string | null | no | For preorder: the shop's promised ship date (ISO 8601). |
image_url | string | null | no | |
thumb_url | string | null | no | A smaller image, when the shop has one. |
description | string | null | no | |
images | string[] | no | The shop's gallery, in the shop's order. |
ProductPage#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
object | "list" | no | |
data | Product[] | sí | |
has_more | boolean | sí | |
next_cursor | string | null | no | Pass as cursor to get the next page. null on the last page. |
Recommendation#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
product | Product | sí | |
why | string | null | no | 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 | no | Who wrote why. null when there is no why. |
ScopeLimit#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
per_minute | integer | sí |
Shelf#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
title | string | sí | |
total | integer | sí | How many products the shelf holds in the shop — not how many are shown in items. |
items | Product[] | sí |
SimilarList#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
object | "list" | no | |
data | Product[] | sí | |
product_id | integer | sí |
SuggestionList#
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
object | "list" | no | |
query | string | sí | |
data | Product[] | sí |
Errores#
Todos los errores tienen la misma forma y un request_id (también en la cabecera X-Request-Id): cítalo cuando nos escribas. Bifurca según code, nunca según message, cuyo texto puede cambiar.
Errores · 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"
}
}| Código | HTTP | Qué significa y qué hacer | Reintento |
|---|---|---|---|
invalid_api_key | 401 | La clave falta, está mal formada, es desconocida o fue revocada. Las cuatro situaciones se ven igual a propósito. Envía Authorization: Bearer <clave> o crea una clave nueva. | corrige la petición |
ip_not_allowed | 403 | La clave tiene una lista de IP permitidas y esta dirección no figura en ella. Llama desde una red de la lista o cambia la lista en el panel. | corrige la petición |
origin_not_allowed | 403 | La llamada viene de una página del navegador cuyo origen no está en la lista de orígenes de la clave. Añade el origen exacto (esquema, host, puerto) en el panel. | corrige la petición |
insufficient_scope | 403 | La clave no tiene el permiso (scope) que exige este endpoint. Los scopes no se pueden ampliar: crea una clave nueva con ese scope. | corrige la petición |
api_disabled | 403 | La API para desarrolladores no está activada para esta tienda. Abre la pestaña Developers del panel o escríbenos si no la ves. | corrige la petición |
subscription_inactive | 402 | La suscripción de la tienda no está activa, así que la API está en pausa. Se reanuda sola cuando la suscripción vuelve a estar activa; las claves siguen siendo válidas. | corrige la petición |
plan_not_entitled | 403 | El plan de la tienda no incluye la API para desarrolladores. Está disponible en los planes que la incluyen. | corrige la petición |
rate_limited | 429 | Se agotó el límite por minuto de esta clave y scope. Espera los segundos de Retry-After y reintenta; reparte las llamadas o guarda resultados en caché. | más tarde |
daily_quota_exceeded | 429 | Se alcanzó el tope diario de respuestas de esta clave (día UTC). Reintenta pasados los segundos de Retry-After, es decir, al cambiar el día. | más tarde |
too_many_failed_attempts | 429 | Llegaron demasiadas claves inválidas desde esta dirección. Espera los segundos de Retry-After y comprueba qué clave envía tu código. | más tarde |
invalid_request | 422 | Falta un parámetro o campo del cuerpo, o está fuera de rango. param indica el primero; corrige la petición, reintentar no ayuda. | corrige la petición |
invalid_cursor | 422 | El cursor fue modificado o no pertenece a esta lista. Pasa next_cursor exactamente como lo recibiste, o empieza de nuevo sin cursor. | corrige la petición |
product_not_found | 404 | No existe ese producto en esta tienda. El id de un producto de otra tienda responde igual: una clave solo lee su propia tienda. | corrige la petición |
idempotency_key_reused | 422 | Este Idempotency-Key ya se usó con un cuerpo distinto. Usa una clave nueva para otra pregunta. | corrige la petición |
idempotency_in_progress | 409 | Una petición con el mismo Idempotency-Key aún se está procesando. Espera un momento y envía la misma petición para obtener su resultado. | más tarde |
request_too_large | 413 | El cuerpo de la petición supera el límite. Envía un cuerpo más pequeño. | corrige la petición |
answer_unavailable | 502 | No se pudo generar la respuesta ahora mismo. Reintenta en breve con el mismo Idempotency-Key. | más tarde |
internal_error | 500 | Algo falló de nuestro lado. Reintenta con una pausa; si persiste, envíanos el request_id. | más tarde |
not_found | 404 | Ese endpoint no existe. Comprueba la ruta con la referencia; las rutas empiezan por la versión, /api/v1. | corrige la petición |
method_not_allowed | 405 | La ruta existe, pero no para este método HTTP. La cabecera Allow lista los permitidos. | corrige la petición |
type agrupa los códigos: authentication_error, permission_error, invalid_request_error, not_found_error, conflict_error, rate_limit_error, billing_error, api_error. Pueden aparecer códigos nuevos dentro de v1; trata un code desconocido según su estado HTTP.
Paginación#
Las listas usan cursores. Pasa next_cursor de una respuesta como cursor en la siguiente petición hasta que next_cursor sea null. limit vale 20 por defecto y como máximo 100. Una página puede tener menos elementos que limit mientras has_more sea true, así que sigue next_cursor en lugar de contar. Trata el cursor como opaco: uno alterado da 422 invalid_cursor.
Caché y ETag#
Las respuestas GET incluyen un ETag. Devuélvelo como If-None-Match; si nada cambió, recibes 304 Not Modified sin cuerpo, más rápido y ligero que descargar la lista otra vez. Guarda en caché las lecturas del catálogo y revalida así.
Idempotencia y reintentos#
POST /answers acepta la cabecera Idempotency-Key (de 1 a 64 caracteres: letras, dígitos y . _ : -). Repetir el mismo cuerpo dentro de 24 horas devuelve la primera respuesta sin una segunda llamada al modelo y sin consumir cuota; la repetición lleva Idempotent-Replayed: true. La misma clave con otro cuerpo da 422 idempotency_key_reused; mientras la primera petición sigue en curso, 409 idempotency_in_progress.
Crea un UUID nuevo para cada pregunta del usuario y reutilízalo solo para reintentos de esa misma pregunta.
Los cuerpos de petición están limitados a 16384 bytes (413 request_too_large).
Navegadores y CORS#
Una página web puede llamar a la API solo desde un origen incluido en la clave (esquema, host y puerto, por ejemplo https://shop.example); la API envía cabeceras CORS solo a los orígenes de la lista. Todo lo que hay en el código de una página es público, así que una clave usada ahí puede copiarla cualquiera que abra la página.
Por eso, para uso en navegador da a la clave solo los scopes suggest y similar más una lista de orígenes, y mantén las claves answer en tu servidor.
Analítica#
El permiso analytics:read abre las cifras de tu propia tienda: los mismos totales, serie diaria, preguntas principales y ventas del asistente que el panel, calculados por el mismo código. Dalo solo a claves que se queden en tu servidor.
Solo se cuentan personas: no se cuentan bots, nuestras propias comprobaciones, llamadas hechas con claves de API ni conversaciones retiradas por el operador. people cuenta visitantes distintos por la marca de su navegador: quien vuelve tres días después es una persona; la misma persona con móvil y portátil, dos; las sessions duran alrededor de un día; los pasos del embudo son sesiones. La facturación total de la tienda no forma parte de la API.
Un periodo es since/until (ISO 8601; sin desfase, UTC) o first_day/last_day, ambos días incluidos, cortados en timezone (por defecto, la zona horaria de la tienda o UTC). La comparación (compare) solo se devuelve cuando el historial de la tienda cubre el periodo de comparación; si no, previous es null.
Texto generado por IA#
En una respuesta, say y cada why los escribe un modelo de IA, y la respuesta lo indica en su objeto ai: generated, las rutas JSON en fields y un texto notice. Si muestras estos textos a tus usuarios, diles que están generados por IA; notice es la redacción mínima que pedimos transmitir a las integraciones.
Los nombres, precios, existencias y enlaces nunca vienen del modelo. Cuando miss es true, la tienda no tiene nada que encaje: muéstralo y no inventes un producto.
Versiones#
La versión forma parte de la ruta: /v1. Dentro de v1 solo hacemos cambios compatibles hacia atrás: nuevos endpoints, nuevos parámetros opcionales, nuevos campos de respuesta y nuevos valores de enumeraciones (por ejemplo nuevos valores de intent). Escribe clientes que ignoren campos desconocidos y que tengan un comportamiento razonable ante valores de enumeración desconocidos. Un cambio incompatible solo se publicaría bajo una nueva ruta de versión.
Para máquinas y asistentes de IA#
La misma referencia en formato legible por máquinas. Importa el documento OpenAPI en Postman, Insomnia o un generador de código.