Skip to main content

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:

  1. 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.
  2. Sync products (POST /api/plugin/shops/{shopId}/products/sync) in batches.
  3. Delete products (DELETE /api/plugin/shops/{shopId}/products/{externalId}) when you remove them from your catalog.
  4. Verify the domain (POST /api/plugin/shops/{shopId}/verify) after registering a shop.
  5. Track conversions (POST /api/plugin/shops/{shopId}/track/conversion) when an order is confirmed.

Common headers

HeaderValueNotes
X-Shop-API-Keysk_…Server-to-server authn. Never to the frontend.
OriginYour storefront URLExtra validation for domain verification.
Content-Typeapplication/jsonFor POST/PUT/PATCH.
User-Agentyour-app/versionMakes log filtering easier on Neuroon.
X-Idempotency-Keystable hash of the batchRecommended on /products/sync: protects against duplicated cron retries. E.g. md5(shopId:productIds:syncType).

Common responses

StatusMeaning
200 / 201OK.
204OK without body (e.g. delete, unverify).
400Validation. Body: { "code": "...", "message": "...", "details": [...] }.
401Invalid or missing API key.
403Valid API key but not authorized for the Origin or shopId.
404Shop / product does not exist.
409Conflict (e.g. domain already verified, drift detected during sync).
429Rate limit. Retry-After (seconds) in the header.
5xxRetry 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/sync and PluginProductDTO. 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 parentExternalId pointing 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 price you send as-is; it does not infer taxes. If your storefront displays prices with VAT, send price with VAT. If without, send without. Stay consistent with what the end customer sees, otherwise the dashboard reports price drift.
  • description accepts 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.
  • syncType accepts INCREMENTAL or FULL. Use INCREMENTAL 99 % of the time: it's an upsert by externalId. Only use FULL during 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

EndpointIdempotent bySafe retries
Sign Widget Token (local)(local signature)Yes: re-signing yields a new valid token.
products/syncexternalId per product + optional X-Idempotency-KeyYes: resending the same batch does not duplicate.
products/{externalId} (DELETE)externalIdYes: deleting twice returns 204.
track/conversionorderIdYes: resending the same conversion does not duplicate.
verifydomain + shopIdYes, idempotent.

Minimum retry pattern: 3 attempts with exponential backoff on 429 and 5xx.

Best practices

  • Never transmit the Shop API Key to the frontend, not even via process.env.NEXT_PUBLIC_*.
  • Cache the Widget Token server-side and refresh with a 5-minute margin.
  • Log with a specific User-Agent to make diagnostics easier.
  • Handle 429 by respecting Retry-After.
  • Don't mix environments: Development tokens won't work against api.neuroon.ai and vice versa.
  • Send X-Idempotency-Key on 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