Shop API Key
The Shop API Key authenticates server-to-server calls against /api/plugin/shops/*. It is used to sync the catalog, record conversions, verify the domain and read shop metadata.
Header
X-Shop-API-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Format
sk_ followed by 32 hexadecimal characters. Total length ~35 characters. It is permanent until rotated: it does not expire by time.
Origin validation
The filter validates the Origin header (or Referer as fallback) against the domain registered for the shop:
- If the client is a browser and sends
Origin, it must matchshop.url. Otherwise →403 Forbiddenwith messageOrigin mismatch — API Key cannot be used from this domain. - If the client does not send Origin or Referer (any server-to-server client: .NET
HttpClient, Pythonrequests, Nodeaxios, cURL, Java HTTP libraries, etc.) → the request is allowed. This is the intended behavior for server-to-server integrations.
Origin validation: three modes
| Client | Origin header | Behavior |
|---|---|---|
Server-to-server (.NET HttpClient, Python requests, cURL, Node axios) | Absent | Allowed — X-Shop-API-Key is the only required credential |
Browser from shop.url | Matches shop.url (normalized) | Allowed |
| Browser from another domain | Differs from shop.url | 403 Origin mismatch — API Key cannot be used from this domain |
URL normalization strips protocol, www. and trailing slashes, so https://example.com, http://www.example.com/ and example.com are considered equivalent.
Note: the Widget Token (
X-Widget-Token, see Widget Token) uses a different validation matrix — it additionally allows trusted domains (neuroon.aiand subdomains,localhost,127.0.0.1). That trusted list does not apply to the Shop API Key, which is only compared againstshop.url.
Generation and rotation
From the Dashboard:
- Shop → API Keys → "Create new key".
- Shown only once. Copy it to your secret manager.
- To rotate: "Rotate key" → the previous one is invalidated immediately.
Storage
Where you should store it:
- .NET: User Secrets in development; Azure Key Vault or
appsettings.{Environment}.jsonmounted as a secret in production. Never in plainweb.configor hardcoded. - WordPress: define in
wp-config.php(which sits outside the docroot) or in encryptedwp_options. - General: AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, GitHub Actions Secrets, Doppler, etc.
Where you must never store it:
- Git repositories (public or private).
- Logs, exported traces, error messages visible to the user.
- HTML attributes (
data-api-key="..."). - Browser-side JavaScript variables.
Examples by stack
cURL
curl -X POST "https://api.neuroon.ai/api/plugin/shops/$SHOP_ID/products/sync" \
-H "X-Shop-API-Key: $NEUROON_API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
Node.js / TypeScript
const res = await fetch(
`${process.env.NEUROON_API_URL}/api/plugin/shops/${SHOP_ID}/products/sync`,
{
method: 'POST',
headers: {
'X-Shop-API-Key': process.env.NEUROON_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
},
);
.NET / C# (any .NET 6+)
var apiKey = builder.Configuration["Neuroon:ApiKey"]
?? throw new InvalidOperationException("Missing Neuroon:ApiKey");
var apiBase = builder.Configuration["Neuroon:ApiBaseUrl"]
?? "https://api.neuroon.ai";
using var client = new HttpClient { BaseAddress = new Uri(apiBase) };
client.DefaultRequestHeaders.Add("X-Shop-API-Key", apiKey);
var response = await client.PostAsJsonAsync(
$"/api/plugin/shops/{shopId}/products/sync", payload);
response.EnsureSuccessStatusCode();
Python
import os, requests
response = requests.post(
f"{os.environ['NEUROON_API_URL']}/api/plugin/shops/{SHOP_ID}/products/sync",
headers={
"X-Shop-API-Key": os.environ["NEUROON_API_KEY"],
"Content-Type": "application/json",
},
json=payload,
timeout=30,
)
response.raise_for_status()
Errors
| Code | Typical message | Cause |
|---|---|---|
401 | Invalid or missing API Key | Missing X-Shop-API-Key or wrong format (not sk_*) |
403 | Origin mismatch — API Key cannot be used from this domain | Browser calling with Origin different from shop.url |
403 | Shop ID mismatch | The sk_* does not belong to the shopId in the URL |
429 | rate_limit_exceeded | Per-endpoint bucket exhausted — see Rate limits |
Further reading
- Widget Token — frontend authentication.
- Rate limits — per-endpoint quotas and backoff.
- Errors — full envelope.