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 server-to-server flows are:
- Sign a Widget Token locally with the Shop API Key (HMAC) and cache it 24 h with a 5 min margin. See Recipe · Server-to-server token.
- Sync products (
POST /api/plugin/shops/{shopId}/products/sync) in batches. - Delete products (
DELETE /api/plugin/shops/{shopId}/products/{externalId}) when you remove them from your catalog. - Verify the domain (
POST /api/plugin/shops/{shopId}/verify) after registering a shop. - Track conversions (
POST /api/plugin/shops/{shopId}/track/conversion) when an 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. |
X-Idempotency-Key | stable hash of the batch | Recommended on /products/sync: protects against duplicated cron retries. E.g. md5(shopId:productIds:syncType). |
Common responses
| Status | Meaning |
|---|---|
200 / 201 | OK. |
204 | OK without body (e.g. delete, 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, drift detected during sync). |
429 | Rate limit. Retry-After (seconds) in the header. |
5xx | Retry with exponential backoff. |
Product sync
Payload semantics
When you build the body for POST /api/plugin/shops/{shopId}/products/sync, there are rules the OpenAPI contract does not make obvious that will silently break your integration:
Full request schema in API Reference →
POST /products/syncandPluginProductDTO. This section only covers the non-obvious points of the contract.
- Variants after the parent. If a product has variants (size/color), send the parent first in the array and each variant with
parentExternalIdpointing to the parent. Orphan variants (no parent in the same batch nor previously synced) are silently dropped. - Tax-inclusive vs tax-exclusive prices — be explicit. The backend takes the
priceyou send as-is; it does not infer taxes. If your storefront displays prices with VAT, sendpricewith VAT. If without, send without. Stay consistent with what the end customer sees, otherwise the dashboard reports price drift. descriptionaccepts up to 5000 characters per the contract. As an operational guideline, very long descriptions (>2000 chars) tend to degrade ranking quality — use the short description if you have one, or truncate to plain text with no HTML.categories[](array). Send the full hierarchy, from the most specific category to the root. The backend uses it to build guided filters.brands[](array). If your platform has a dedicated brand field, send it here. Don't derive it from the title.syncTypeacceptsINCREMENTALorFULL. UseINCREMENTAL99 % of the time: it's an upsert byexternalId. Only useFULLduring an initial migration or a deliberate reset — it wipes the catalog in Neuroon before reinserting the batch you send. Never trigger it from cron: an accidental FULL empties the index for the duration of the sync.- Batch size: max 500 products per request, min 1.
Conflict detection before overwriting
If your integration shares the catalog with another source (Neuroon dashboard, another integration, manual edits), read before writing:
async function syncProductSafely(local) {
const remote = await fetch(
`${API_URL}/api/plugin/shops/${SHOP_ID}/products/${local.externalId}`,
{ headers: { 'X-Shop-API-Key': API_KEY } },
).then(r => r.ok ? r.json() : null);
if (remote && hasDriftedFromLastKnown(local.lastSyncedSnapshot, remote)) {
// The product changed in Neuroon since your last sync — request confirmation
// before overwriting, or log the conflict for review.
return { status: 'conflict', remote };
}
return postSync([local]);
}
Fields worth diffing: name, price, salePrice, description, sku, stockQuantity, stockStatus. Without this check, edits made in the dashboard (e.g. the user fixes a price) are silently overwritten on the next cron sync.
Deleting products from the catalog
POST /products/sync does not handle deletions — products you stop sending stay in Neuroon. To remove a product, call explicitly:
curl -X DELETE "https://api.neuroon.ai/api/plugin/shops/$SHOP_ID/products/$EXTERNAL_ID" \
-H "X-Shop-API-Key: $API_KEY"
Recommended pattern for integrations that don't have a direct "product deleted" signal:
// Nightly job: detect SKUs deleted from your DB that still live in Neuroon
async function reconcileDeletions() {
const localSkus = new Set((await db.query('SELECT external_id FROM products WHERE deleted = false')).map(r => r.external_id));
let cursor = null;
do {
const { products, nextCursor } = await fetch(
`${API_URL}/api/plugin/shops/${SHOP_ID}/products?cursor=${cursor ?? ''}`,
{ headers: { 'X-Shop-API-Key': API_KEY } },
).then(r => r.json());
for (const p of products) {
if (!localSkus.has(p.externalId)) {
await fetch(
`${API_URL}/api/plugin/shops/${SHOP_ID}/products/${p.externalId}`,
{ method: 'DELETE', headers: { 'X-Shop-API-Key': API_KEY } },
);
}
}
cursor = nextCursor;
} while (cursor);
}
Recommended frequency: once a day. Without it, products removed from your storefront keep showing up in search.
Signing the Widget Token
Your server signs the token locally with the Shop API Key as the HMAC secret. You don't call Neuroon for this.
Copy-paste implementations with cache + rotation (Node, .NET, Python, PHP) in Recipe · Server-to-server token.
Idempotency and retries
| Endpoint | Idempotent by | Safe retries |
|---|---|---|
| Sign Widget Token (local) | (local signature) | Yes: re-signing yields a new valid token. |
products/sync | externalId per product + optional X-Idempotency-Key | Yes: resending the same batch does not duplicate. |
products/{externalId} (DELETE) | externalId | Yes: deleting twice returns 204. |
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. - Don't mix environments: Development tokens won't work against
api.neuroon.aiand vice versa. - Send
X-Idempotency-Keyon sync batches: protects against duplicated cron retries after a timeout. - Implement the deletions job: without it, your Neuroon catalog grows forever.
- Diff before overwriting if the catalog is edited from more than one source.
Next steps
- Recipe · Server-to-server token — guided version with Redis cache.
- Recipe · Conversion tracking — end-to-end attribution with cookie + searchLogId.
- Authentication · Shop API Key.
- Authentication · Widget Token.
- Authentication · Rate Limits.