Custom · Server-to-server
This is the base pattern for any custom integration: your backend talks to Neuroon using the Shop API Key (sk_…) and never exposes that key to the frontend. The three server-to-server flows are:
- Issue a Widget Token (
POST /api/shops/{id}/widget-token) and cache it. TTL is fixed at 24 h; rotate at 23 h. - Sync products (
POST /api/plugin/shops/{shopId}/products/sync) in batches. - Track conversions (
POST /api/plugin/shops/{shopId}/track/conversion) when the order is confirmed.
Common headers
| Header | Value | Notes |
|---|---|---|
X-Shop-API-Key | sk_… | Server-to-server authn. Never to the frontend. |
Origin | Your storefront URL | Extra validation for domain verification. |
Content-Type | application/json | For POST/PUT/PATCH. |
User-Agent | your-app/version | Makes log filtering easier on Neuroon. |
Common responses
| Status | Meaning |
|---|---|
200 / 201 | OK. |
204 | OK without body (e.g. unverify). |
400 | Validation. Body: { "code": "...", "message": "...", "details": [...] }. |
401 | Invalid or missing API key. |
403 | Valid API key but not authorized for the Origin or shopId. |
404 | Shop / product does not exist. |
409 | Conflict (e.g. domain already verified). |
429 | Rate limit. Retry-After (seconds) in the header. |
5xx | Retry with exponential backoff. |
Example: issuing a Widget Token
import fetch from 'node-fetch';
const SHOP_ID = process.env.NEUROON_SHOP_ID;
const API_KEY = process.env.NEUROON_API_KEY;
const API_URL = process.env.NEUROON_API_URL ?? 'https://api.neuroon.ai';
const ROTATE_AFTER_MS = 23 * 60 * 60 * 1000; // TTL fixed at 24 h
let cache = null; // { token, issuedAt: epoch ms }
export async function getWidgetToken() {
const now = Date.now();
if (cache && now - cache.issuedAt < ROTATE_AFTER_MS) return cache.token;
const res = await fetch(`${API_URL}/api/shops/${SHOP_ID}/widget-token`, {
method: 'POST',
headers: { 'X-Shop-API-Key': API_KEY, 'User-Agent': 'my-app/1.0' },
});
if (!res.ok) throw new Error(`Token issuance failed: ${res.status}`);
const body = await res.json();
cache = { token: body.token, issuedAt: now };
return cache.token;
}Verify the domain (without the plugin)
If your integration is custom (not using the WordPress plugin), you need to verify your domain once before you can issue Widget Tokens. Verification ties your shop.url to the domain that will serve the widget.
Call POST /api/plugin/shops/{shopId}/verify with the public storefront domain in the body:
curl -X POST "$NEUROON_API_URL/api/plugin/shops/$NEUROON_SHOP_ID/verify" \
-H "X-Shop-API-Key: $NEUROON_API_KEY" \
-H "Content-Type: application/json" \
-H "Origin: https://mystore.com" \
-d '{"domain":"https://mystore.com"}'
The body field is
domain(@NotBlankinShopRequestDTO.java:60). Sendingurlreturns a 400.
The backend compares the submitted domain against the shop.url registered in the dashboard. If they match, the domain is verified and tokens become servable. If not:
403 Forbiddenwith the standard envelope{ timestamp, status, error, message, path }— editshop.urlin the dashboard and retry.404 Not Foundif theshopIddoes not exist or does not belong to your API Key.409 Conflictif the domain is already verified by another shop — open a support ticket.
To inspect verification state without mutating anything:
curl "$NEUROON_API_URL/api/plugin/shops/$NEUROON_SHOP_ID/verification-data" \
-H "X-Shop-API-Key: $NEUROON_API_KEY"
Returns { verificationCode, instructions, verified } (VerificationDataDTO). The POST verify additionally returns { shopId, verificationCode, domain, name } (PluginVerificationDataDTO).
In staging you can verify as many domains as you need by repeating
POST /verifywith eachdomain. In Production every domain change requires updatingshop.urlin the dashboard.
Widget Token rotation
There is no "refresh" endpoint for Widget Tokens. Each Widget Token lives 24 h (app.widget.token.ttl-hours = 24 in application.yml:271). When you need to rotate:
- Call
POST /api/shops/{shopId}/widget-tokenagain with your Shop API Key. - Start using the new token. The previous one remains valid until it expires — there is no instant revocation.
- Cache with a ≤ 23 h margin on your side (see the pattern at the top of this page).
The
POST /api/tokens/refreshendpoint you see in the OpenAPI reference does not rotate Widget Tokens: it refreshes user JWTs for the Neuroon dashboard (TokenController.java:36). Don't use it for your widget integration.
Idempotency and retries
| Endpoint | Idempotent by | Safe retries |
|---|---|---|
widget-token | (no) | Yes: every call issues a new token (rotates). |
products/sync | externalId per product | Yes: resending the same batch does not duplicate. |
track/conversion | orderId | Yes: resending the same conversion does not duplicate. |
verify | domain + shopId | Yes, idempotent. |
Minimum retry pattern: 3 attempts with exponential backoff on 429 and 5xx.
Best practices
- Never transmit the
Shop API Keyto the frontend, not even viaprocess.env.NEXT_PUBLIC_*. - Cache the Widget Token server-side and refresh with a 5-minute margin.
- Log with a specific
User-Agentto make diagnostics easier. - Handle
429by respectingRetry-After. - Do not mix environments: Development tokens do not work against
api.neuroon.aiand vice versa.
Next steps
- Recipe · Server-to-server token — guided version with Redis cache.
- Authentication · Shop API Key.
- Authentication · Widget Token.
- Authentication · Rate Limits.