API conventions
This page summarizes the rules common to every endpoint. If your integration doesn't follow them, you will get 400 Bad Request, 401 Unauthorized, or the typical {"timestamp":..., "status":..., "error":..., "message":..., "path":...}.
Base URLs
| Environment | Base URL | When |
|---|---|---|
| Production | https://api.neuroon.ai | Real traffic, persistent data, conversions that count |
| Development | https://dev-api.neuroon.ai | Integration testing, error payloads, edge cases without risk |
No URL versioning today — there is no /v1/ or /v2/. Breaking changes are announced in advance in the Changelog and applied to Development first. URL versioning will land later as an additive change (current routes will keep working through a grace window).
sk_* keys and Widget Tokens issued in Development do not work against Production and vice versa. Use separate environment variables:
NEUROON_API_URL=https://dev-api.neuroon.ai
NEUROON_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEUROON_SHOP_ID=shop_dev_xxxxxxxx
See Switching environments for per-stack configuration.
Format
- Request body: JSON with
Content-Type: application/json; charset=utf-8.camelCaseon every field. - Response body: JSON
camelCase. Keys are always strings; numbers may beint,long,decimal(prices). Dates are ISO 8601 (2026-05-06T10:15:00Z, or withoutZif local — the backend returns UTC without offset). - Charset: UTF-8. Spring decodes the body as UTF-8 — bytes that are not valid UTF-8 break the JSON parser with
400.
Mandatory headers
| Header | Applies to | Value |
|---|---|---|
Content-Type | requests with body | application/json |
Accept | recommended | application/json |
Authorization or X-Widget-Token or X-Shop-API-Key | depending on endpoint | See Authentication · Overview |
Useful optional headers:
Accept-Language: es-ES,en-US,fr-FR, etc. — the backend uses this value for generated text (guided filters, AI assistant) when it matches one of the 9 supported locales; otherwise it falls back toes.User-Agent: YourApp/1.2.3— for your own log analysis (recommended).
Idempotency
POST /api/plugin/shops/{shopId}/products/syncis idempotent byexternalId: sending the sameexternalIdtwice updates the product, it does not duplicate. This includesINCREMENTALmode (default).FULLmode wipes the full catalog from the index before reinserting — use it only for initial migrations.POST /api/plugin/shops/{shopId}/track/conversionaccepts a uniqueorderIdper shop; a second POST with the sameorderIdis silently ignored (it does not duplicate metrics).POST /api/widget/track/clickandPOST /api/widget/track/conversionare identified bysearchLogId + productId + timestamp; retries are safe.- The remaining endpoints are
GET(no side effects) or stateless.
Latency and consistency
- Product indexing: 2-5 seconds after
200 OKonproducts/sync. The product is not searchable immediately. If your integration test doesGET /products/{externalId}right after the sync, wait ~5 s or poll with backoff. - Quotas:
productsCountandsearchesThisMonthinGET /shops/meare cached ~5 min by some consumers (the WordPress plugin, for example). The backend always returns the up-to-date value. - Tokens: the Widget Token has a 24 h TTL (86400 s). Cache it on your server with a 5-minute safety margin before expiration.
Error shape
The standard envelope is emitted by GlobalExceptionHandler for almost every error:
{
"timestamp": "2026-05-06T10:15:00",
"status": 400,
"error": "Bad Request",
"message": "externalId: must not be blank, price: must be greater than or equal to 0.0",
"path": "/api/plugin/shops/shop_abc/products/sync"
}
status: numeric HTTP code.error: canonical status name.message: field-level errors comma-joined; human-readable.path: request path (without host).
Special cases with a different envelope:
429(rate limit) —WidgetRateLimitFilteremits{ error, message, retryAfter, limit, remaining, timestamp }. Nostatus, nopath.401/403from auth filters —ApiKeyAuthenticationFilteremits{ error, status }without going through the global handler.
Detail per code at Reference · Errors.
Rate limits
Throttling per endpoint, not per subscription plan:
| Endpoint | Limit |
|---|---|
widget-search | 200 req/min |
widget-suggestions | 300 req/min |
widget-analytics | 500 req/min |
widget-compare | 10 req/min |
sync (plugin) | 100 req/min |
shop-info | 60 req/min |
verification | 20 req/min |
Response headers: X-RateLimit-Limit, X-RateLimit-Remaining. On 429, also Retry-After (seconds).
Detail and exponential backoff at Rate limits.
Pagination
GET /api/plugin/shops/{shopId}/products and derived endpoints accept:
page(0-indexed, default0).size(default20, maximum100).sort(formatfield,direction, e.g.name,asc).modifiedAfter(ISO 8601, for incremental delta sync).
Paginated response:
{
"content": [...],
"page": 0,
"size": 20,
"totalElements": 1247,
"totalPages": 63,
"first": true,
"last": false
}
CORS
/api/widget/*: CORS open to any origin (it is JS from customer browsers); credentials via theX-Widget-Tokenheader whoseOriginis validated againstapp.widget.allowed-originsandshop.url./api/plugin/*: CORS disabled by design — these are server-to-server calls. If you call from a browser and your Origin doesn't matchshop.url, you get403./api/search/*: CORS configured for the dashboard and*.neuroon.aisubdomains.
If your server-to-server integration goes through a proxy that automatically adds Origin and breaks validation, configure the proxy not to send the header.
Further reading
- Authentication · Overview — which credential to use.
- Rate limits — full table and backoff.
- Reference · Errors — every code.
- Reference · Data models — real schemas.