External API
Give external partners, ERP systems, and scripts direct access to your product catalog through a lightweight, secure REST API.
The External API (/api/v1/ext/*) is designed for machine-to-machine integrations where a full user session is impractical. All requests authenticate with an API key carrying explicit scopes. The response format is consistent across every endpoint.
Overview
| Base URL | https://api.pixeepim.com/api/v1/ext |
|---|---|
| Auth | X-API-Key header |
| Read scope | ext:products:read |
| Write scope | ext:products:write |
| Rate limit | Standard tier (see Rate Limiting) |
| Max batch size | 500 products |
| Contract version | API-Version: 1 on every response |
Create External API keys in Settings → API Keys. Select only the scopes
your integration needs — ext:products:read for read-only access,
ext:products:write to create or modify products.
Authentication
Pass the API key in the X-API-Key request header on every call. There is no session or token exchange.
All requests
curl https://api.pixeepim.com/api/v1/ext/products \
-H "X-API-Key: pm_live_abc123..."
All read endpoints require ext:products:read. Write endpoints (POST, PUT, PATCH) require ext:products:write.
Versioning
/api/v1/ext/* and /api/v1/windev/* form the public contract, major version 1. Its frozen OpenAPI description is published as openapi-public-v1.json (security scheme APIKeyHeader, header X-API-Key); any breaking change against that file must be announced in the changelog before it ships.
API-Version: 1is set on every response of both families — successes and errors (missing or refused key, not found, validation, quota, server error). It carries the major version of the contract, independent of the application version.GET /ext/statusreturns the same value indata.api_version. The olderdata.versionfield ("1.0") is kept for compatibility and frozen — it follows neither the contract nor the application; readapi_version.- A deprecated operation will carry
Deprecation,SunsetandLink: <https://apidoc.pixeepim.com/changelog>; rel="deprecation"headers, with at least 180 days' notice before removal. No operation is deprecated today.
See Changelog → API versioning for what counts as a breaking change.
Response envelope
Every response wraps the payload in a consistent envelope:
Success envelope
{
"success": true,
"data": { ... },
"meta": {
"total": 1250,
"page": 1,
"page_size": 50,
"updated_at": "2026-06-19T10:00:00Z"
},
"errors": []
}
For list endpoints, data is an array and meta.total holds the full count. For single-resource endpoints, data is an object.
Errors (401, 403, 404, 409, 422, 500) use the platform's standard error envelope, not the success envelope — branch on the HTTP status and error.error_code:
Error envelope
{
"error": {
"code": 422,
"error_code": "VALIDATION_URL_SLUG_TOO_LONG",
"type": "validation_error",
"message": "…",
"details": null,
"path": "/api/v1/ext/products",
"request_id": "abc-123",
"timestamp": "2026-10-21T10:00:00Z"
}
}
A 429 uses the divergent rate-limit shape described in Rate Limiting.
Unknown fields
Write operations (POST /ext/products, PUT and PATCH /ext/products/{ean}, POST /ext/products/batch) tolerate keys they do not know: nothing is refused and nothing is written for them. They are no longer dropped silently — the envelope lists them, sorted, in champs_ignores (empty list when there are none). A misspelt name (brandName instead of brand_name) is therefore visible in the response.
On a batch, champs_ignores is the union of the ignored keys of all products, and champs_ignores_par_fiche gives the detail, one entry per product concerned: { "index": 0, "ean": "…", "champs": ["…"] } (index from 0). Reads that share the envelope (GET /ext/products/{ean}…) carry both keys as null.
Envelope of a write
{
"success": true,
"data": { ... },
"meta": { ... },
"errors": [],
"champs_ignores": ["brandName", "category_id"]
}
Unknown keys stay tolerated for the whole life of contract v1. Refusing them (422 REQUEST_UNKNOWN_FIELD, as the app's internal routes already do) will only come with a v2 of the public contract (/api/v2/…), never in v1.
Brands
brand_name is matched against your brand directory: exact name, ignoring case and accents, an alias, or the name without its legal suffix (« Sony Corp. » → « Sony »). When it matches, the product is filed under that brand and receives the brand's name as written in the directory (so brand_name read back may differ in case from what you sent). A name that is unknown, carried by several brands, or by a disabled brand is kept as is, without an attached brand — no brand is ever created.
- Changing the name detaches the previous brand.
PUTwithbrand_nameabsent ornulldetaches the brand;PATCHwithbrand_name: ""does too;PATCHwithbrand_nameabsent ornullleaves the brand untouched.POST /ext/products/batchreportsdata.brands_unresolved: the number of saved products whosebrand_namematched no brand.
Product cap
Since October 20, 2026, creating a product through /ext (and WinDev) respects the product cap of your plan, exactly like the app screens and imports. An update is never refused by the cap.
| Plan | Product cap |
|---|---|
| Free | 2 000 |
| Starter | 10 000 |
| Growth | 50 000 |
| Scale | 100 000 |
| Enterprise | Unlimited |
A license that carries a negotiated cap keeps it. At the cap:
POST /ext/productsanswers403witherror_codeBILLING_LICENSE_QUOTA_EXCEEDED— nothing is written. If another creation was checking the cap at the same moment, it answers409BILLING_LICENSE_QUOTA_BUSYinstead: retry the request.POST /ext/products/batchrefuses only the products to be created beyond the cap, each with a named reason inerror_details; the other products of the batch (updates included) are processed.
Status
Health check for the External API. Returns the authenticated key's name and assigned scopes, and api_version — the major version of the public contract (same value as the API-Version header). version is a frozen historical value; read api_version.
Requires ext:products:read.
Request
curl https://api.pixeepim.com/api/v1/ext/status \
-H "X-API-Key: pm_live_abc123..."
Response
{
"success": true,
"data": {
"status": "ok",
"version": "1.0",
"api_version": "1",
"api_key_name": "ERP Sync Key",
"api_key_scopes": ["ext:products:read", "ext:products:write"],
"timestamp": "2026-06-19T10:00:00Z"
},
"meta": { "total": 1, "page": 1, "page_size": 1, "updated_at": "..." },
"errors": []
}
List products
Returns a paginated list of products. Use updated_since to implement incremental polling, or supplier_code to filter by origin.
Query parameters
- Name
supplier_code- Type
- string
- Description
Filter by import source / supplier code (e.g.
KIMEX,INGRAM).
- Name
updated_since- Type
- string
- Description
ISO 8601 datetime — returns only products modified after this date. Example:
2026-01-01T00:00:00Z.
- Name
page- Type
- integer
- Description
Page number, starting at
1(default:1).
- Name
page_size- Type
- integer
- Description
Items per page, max
500(default:50).
Request
curl "https://api.pixeepim.com/api/v1/ext/products?page=1&page_size=100&updated_since=2026-06-01T00:00:00Z" \
-H "X-API-Key: pm_live_abc123..."
Response
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"ean": "3760000000001",
"title": "Support TV mural fixe",
"brand_name": "KIMEX",
"is_active": true,
"status": "active",
"weight": 1.5,
"updated_at": "2026-06-19T09:30:00Z"
}
],
"meta": {
"total": 1250,
"page": 1,
"page_size": 100,
"updated_at": "2026-06-19T10:00:00Z"
},
"errors": []
}
Delta sync
Dedicated endpoint for incremental synchronisation. Returns all products created or updated since the given since datetime. Preferred over /products?updated_since= for sync workflows because the intent is explicit and the parameter is required.
Query parameters
- Name
since- Type
- string
- Description
ISO 8601 datetime (e.g.
2026-06-18T00:00:00Z). Products modified at or after this timestamp are returned.
- Name
page- Type
- integer
- Description
Page number (default:
1).
- Name
page_size- Type
- integer
- Description
Items per page, max
500(default:100).
Store the updated_at from meta in your last-sync timestamp. Pass it as
since on the next call to process only new changes.
Request
curl "https://api.pixeepim.com/api/v1/ext/products/delta?since=2026-06-18T00:00:00Z" \
-H "X-API-Key: pm_live_abc123..."
Polling loop (Python)
import httpx, json
from datetime import datetime, timezone
API_KEY = "pm_live_abc123..."
BASE = "https://api.pixeepim.com/api/v1/ext"
last_sync = "2026-01-01T00:00:00Z"
while True:
r = httpx.get(
f"{BASE}/products/delta",
params={"since": last_sync, "page_size": 500},
headers={"X-API-Key": API_KEY},
)
body = r.json()
products = body["data"]
last_sync = body["meta"]["updated_at"]
process(products) # your integration logic
if len(products) < 500:
break # no more pages
Get a product
Retrieves a single product by its EAN.
Path parameters
- Name
ean- Type
- string
- Description
EAN-13 (or equivalent) of the product to retrieve.
Request
curl https://api.pixeepim.com/api/v1/ext/products/3760000000001 \
-H "X-API-Key: pm_live_abc123..."
Response
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"ean": "3760000000001",
"title": "Support TV mural fixe",
"brand_name": "KIMEX",
"status": "active",
"is_active": true,
"weight": 1.5,
"created_at": "2026-03-01T08:00:00Z",
"updated_at": "2026-06-19T09:30:00Z"
},
"meta": { "total": 1, "page": 1, "page_size": 1, "updated_at": "..." },
"errors": []
}
Create or upsert
Creates a new product or updates an existing one, matched by EAN.
- EAN not found → creates the product (
nameis required). The creation counts against your plan's product cap. - EAN already exists → updates non-null fields from the payload.
Requires ext:products:write.
Errors
| Status | error_code | When |
|---|---|---|
422 | VALIDATION_URL_SLUG_TOO_LONG | url_slug longer than 255 characters |
422 | VALIDATION_URL_SLUG_SCHEME | url_slug carries a scheme (looks like a full address, e.g. javascript:) — send only the end of the address (my-product-42) |
422 | — (field named in error.details) | weight ≥ 10 000 000, or width / height / depth ≥ 100 000 000, or NaN / infinity |
403 | BILLING_LICENSE_QUOTA_EXCEEDED | Creation refused: the plan's product cap is reached |
409 | BILLING_LICENSE_QUOTA_BUSY | Another creation was checking the cap at the same moment — retry |
Body parameters
- Name
ean- Type
- string
- Description
EAN-13 or equivalent. Used as the unique key for upsert matching.
- Name
name- Type
- string
- Description
Product title. Required when creating (EAN not found).
- Name
brand_name- Type
- string
- Description
Brand name, matched against your brand directory — never created (see Brands).
- Name
description- Type
- string
- Description
Long description (HTML allowed).
- Name
short_description- Type
- string
- Description
Short marketing description.
- Name
supplier_code- Type
- string
- Description
Import source identifier (e.g.
KIMEX).
- Name
reference- Type
- string
- Description
Manufacturer reference (not the internal
sku, which is read-only here).
- Name
status- Type
- string
- Description
active,inactive, orarchived.
- Name
weight- Type
- number
- Description
Weight in kg — lower than 10 000 000.
- Name
width- Type
- number
- Description
Width in cm — lower than 100 000 000.
- Name
height- Type
- number
- Description
Height in cm — lower than 100 000 000.
- Name
depth- Type
- number
- Description
Depth in cm — lower than 100 000 000.
- Name
url_slug- Type
- string
- Description
SEO slug — the end of the address only, 255 characters max.
See Product fields for the full list.
Request
curl -X POST https://api.pixeepim.com/api/v1/ext/products \
-H "X-API-Key: pm_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"ean": "3760000000001",
"name": "Support TV mural fixe 37-75 pouces",
"brand_name": "KIMEX",
"supplier_code": "KIMEX",
"weight": 1.5,
"status": "active"
}'
Response
{
"success": true,
"data": {
"created": true,
"ean": "3760000000001",
"id": "550e8400-e29b-41d4-a716-446655440000",
"product": { ... }
},
"meta": { "total": 1, "page": 1, "page_size": 1, "updated_at": "..." },
"errors": [],
"champs_ignores": []
}
Batch upsert
Upserts up to 500 products in a single request. Each product is processed independently — individual errors are collected in error_details without interrupting the rest of the batch.
Requires ext:products:write.
Refused per product
These refuse only the product concerned (an entry in error_details with its index, ean and a named reason); the other products of the batch are written:
- a
url_sluglonger than 255 characters; - a
weight,width,heightordepthout of range (same bounds as the single upsert); - a creation beyond the plan's product cap (updates are never refused by the cap);
- an unexpected error on that product: the reason says the product was not saved and gives a reference to pass on to support. It never contains technical error text.
Refused for the whole request
A url_slug that looks like a full address — a : before the first /, such as javascript:, including encoded forms (:, :, full-width colon) — in any one product refuses the entire request on entry: 422 VALIDATION_URL_SLUG_SCHEME, no product of the batch is saved. Send only the end of the address (my-product-42), then resend the batch.
Body parameters
- Name
products- Type
- array
- Description
Array of product objects (max 500). Each item follows the same schema as the single upsert endpoint.
Request
curl -X POST https://api.pixeepim.com/api/v1/ext/products/batch \
-H "X-API-Key: pm_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"products": [
{ "ean": "3760000000001", "name": "Produit A", "brand_name": "KIMEX" },
{ "ean": "3760000000002", "name": "Produit B", "brand_name": "KIMEX", "color": "noir" },
{ "ean": "3760000000003", "name": "Produit C", "url_slug": "…300 characters…" }
]
}'
Response
{
"success": true,
"data": {
"total": 3,
"created": 1,
"updated": 1,
"errors": 1,
"brands_unresolved": 0,
"error_details": [
{ "index": 2, "ean": "3760000000003", "error": "…" }
]
},
"meta": { "total": 3, "page": 1, "page_size": 3, "updated_at": "..." },
"errors": [],
"champs_ignores": ["color"],
"champs_ignores_par_fiche": [
{ "index": 1, "ean": "3760000000002", "champs": ["color"] }
]
}
Full update (PUT)
Fully replaces a product's writable fields. Fields not present in the payload are set to null (a missing or null brand_name also detaches the brand). Returns 404 if the EAN does not exist (unlike POST /products which creates).
Requires ext:products:write.
Path parameters
- Name
ean- Type
- string
- Description
EAN of the product to replace.
The body follows the same schema as POST /products (see Product fields), with the same limits: url_slug at most 255 characters (422 VALIDATION_URL_SLUG_TOO_LONG) and the same range for weight, width, height, depth (422, field named). A replacement is an update: it is never refused by the product cap.
Request
curl -X PUT https://api.pixeepim.com/api/v1/ext/products/3760000000001 \
-H "X-API-Key: pm_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"ean": "3760000000001",
"name": "Support TV mural fixe — édition 2026",
"brand_name": "KIMEX",
"weight": 1.6,
"status": "active"
}'
Response
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"ean": "3760000000001",
"title": "Support TV mural fixe — édition 2026",
"updated_at": "2026-06-19T10:00:00Z"
},
"meta": { "total": 1, "page": 1, "page_size": 1, "updated_at": "..." },
"errors": []
}
Partial update (PATCH)
Updates only the fields provided. Null or omitted fields are left unchanged. Returns 404 if the EAN does not exist.
Requires ext:products:write.
Path parameters
- Name
ean- Type
- string
- Description
EAN of the product to partially update.
The body accepts any subset of product fields (all optional for PATCH), with the same limits: url_slug at most 255 characters (422 VALIDATION_URL_SLUG_TOO_LONG) and the same range for weight, width, height, depth (422, field named).
Request
curl -X PATCH https://api.pixeepim.com/api/v1/ext/products/3760000000001 \
-H "X-API-Key: pm_live_abc123..." \
-H "Content-Type: application/json" \
-d '{ "status": "inactive", "weight": 1.6 }'
Response
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"ean": "3760000000001",
"status": "inactive",
"weight": 1.6,
"updated_at": "2026-06-19T10:05:00Z"
},
"meta": { "total": 1, "page": 1, "page_size": 1, "updated_at": "..." },
"errors": []
}
Product fields
The following fields are accepted by POST, PUT, and PATCH endpoints. On single-product writes (POST /products, PUT, PATCH) an out-of-range value refuses the request (422); in POST /products/batch it refuses only that product (see Batch upsert).
| Field | Type | Notes |
|---|---|---|
ean | string | EAN-13 or equivalent — required on POST |
name | string | Product title — required when creating via POST |
brand_name | string | Matched against the brand directory, never created — see Brands |
description | string | Long description (HTML allowed) |
short_description | string | Short marketing text |
reference | string | Manufacturer reference |
supplier_code | string | Import source code (e.g. KIMEX) |
category_id | — | No longer written since October 30, 2026 (v6.33.0): the key is ignored and cited in champs_ignores; read back, it is always null. Categories are managed in the Category Manager |
asin | string | Amazon ASIN |
upc | string | Universal Product Code |
gtin | string | Global Trade Item Number |
status | string | active · inactive · archived |
is_active | boolean | Shorthand for status = active |
weight | number | Weight in kg — lower than 10 000 000; NaN and infinity refused |
width | number | Width in cm — lower than 100 000 000 |
height | number | Height in cm — lower than 100 000 000 |
depth | number | Depth in cm — lower than 100 000 000 |
unit_of_measure | string | unit, pack, m, kg, … |
external_id | string | Your system's internal ID |
url_slug | string | SEO slug for e-commerce — the end of the address only, 255 characters max, no scheme (javascript:…) |
Read-only fields
Returned by GET /ext/products, GET /ext/products/{ean}, GET /ext/products/delta and by the writes that return the product. Sent in a write, they are ignored (and cited in champs_ignores) like any unknown key.
| Field | Type | Notes |
|---|---|---|
sku | string | Internal reference of the product, unique per tenant — neither reference (manufacturer) nor a supplier reference |
condition | string | new · refurbished · used; null when not set |
net_content | number | Net content, for the unit price |
net_content_unit | string | ml · cl · l · g · kg · cm · m · m2 · m3 · unite |
gross_weight | number | Gross weight in kg, packaging included (weight is the net weight) |
fabricant | object | Product safety (EU GPSR, Regulation 2023/988): the resolved manufacturer — the product's own if it names one, else its brand's. provenance = produit or marque; raison_sociale, nom_commercial, address, pays, courriel, telephone, site_web, and personne_responsable (responsible person in the EU, same shape). null when neither the product nor its brand names one |
avertissements_securite | object | Safety warnings per ISO 639-1 language: { "fr": "…", "en": "…" } |
A change to a brand's manufacturer, or to the safety warnings, does not modify the product's updated_at: GET /ext/products/delta does not see it.
API key management
API keys used by the External API (and the rest of the platform) can also be managed programmatically. These endpoints require a JWT Bearer session (they are account-management operations, not part of the /ext/* M2M surface) and the MANAGE_SETTINGS permission.
List API keys
Returns your account's API keys (hashed — the raw value is never returned again after creation).
Query parameters
- Name
include_inactive- Type
- boolean
- Description
Include revoked/inactive keys.
- Name
page- Type
- integer
- Description
Page number (default:
1).
- Name
per_page- Type
- integer
- Description
Items per page.
Request
curl https://api.pixeepim.com/api/v1/api-keys \
-H "Authorization: Bearer {jwt_access_token}"
Create an API key
Creates a new API key. The raw key is returned once, in the response — store it immediately.
Body parameters
- Name
name- Type
- string
- Description
Descriptive name (e.g.
ERP Sync Key).
- Name
description- Type
- string
- Description
Optional description.
- Name
scopes- Type
- array
- Description
List of scopes (see Authentication for the 14 available scopes). Default:
["products:read", "ext:products:read"].
- Name
expires_at- Type
- string
- Description
ISO 8601 expiry datetime. Omit for a key that never expires.
- Name
rate_limit_per_minute- Type
- integer
- Description
Optional per-key rate limit override (
1–10000).
- Name
is_test- Type
- boolean
- Description
truefor apm_test_key,false(default) for apm_live_production key.
Request
curl -X POST https://api.pixeepim.com/api/v1/api-keys \
-H "Authorization: Bearer {jwt_access_token}" \
-H "Content-Type: application/json" \
-d '{
"name": "ERP Sync Key",
"scopes": ["ext:products:read", "ext:products:write"],
"is_test": false
}'
Response
{
"id": "key-001",
"name": "ERP Sync Key",
"key": "pm_live_abc123...",
"scopes": ["ext:products:read", "ext:products:write"],
"created_at": "2026-09-17T10:00:00Z"
}
Get an API key
Returns metadata for a single API key (never the raw key value).
Path parameters
- Name
key_id- Type
- string
- Description
The API key ID.
Request
curl https://api.pixeepim.com/api/v1/api-keys/key-001 \
-H "Authorization: Bearer {jwt_access_token}"
Update an API key
Updates a key's name, description, scopes, expiry, rate limit, or active status.
Path parameters
- Name
key_id- Type
- string
- Description
The API key ID.
Body parameters
- Name
name- Type
- string
- Description
- Updated name.
- Name
description- Type
- string
- Description
- Updated description.
- Name
scopes- Type
- array
- Description
- Updated scope list.
- Name
expires_at- Type
- string
- Description
- Updated expiry.
- Name
rate_limit_per_minute- Type
- integer
- Description
- Updated per-key rate limit.
- Name
is_active- Type
- boolean
- Description
- Enable or disable the key without deleting it.
Request
curl -X PATCH https://api.pixeepim.com/api/v1/api-keys/key-001 \
-H "Authorization: Bearer {jwt_access_token}" \
-H "Content-Type: application/json" \
-d '{"is_active": false}'
Revoke an API key
Permanently revokes an API key. Requests using it are rejected immediately.
Path parameters
- Name
key_id- Type
- string
- Description
The API key ID.
Request
curl -X DELETE https://api.pixeepim.com/api/v1/api-keys/key-001 \
-H "Authorization: Bearer {jwt_access_token}"
Rotate an API key
Issues a new raw key value for an existing key ID and immediately invalidates the old value. Scopes and settings are preserved.
Path parameters
- Name
key_id- Type
- string
- Description
The API key ID.
Query parameters
- Name
is_test- Type
- boolean
- Description
Switch the key between
pm_test_andpm_live_prefix on rotation.
Request
curl -X POST https://api.pixeepim.com/api/v1/api-keys/key-001/rotate \
-H "Authorization: Bearer {jwt_access_token}"
Response
{
"id": "key-001",
"key": "pm_live_def456...",
"rotated_at": "2026-09-17T11:00:00Z"
}