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 URLhttps://api.pixeepim.com/api/v1/ext
AuthX-API-Key header
Read scopeext:products:read
Write scopeext:products:write
Rate limitStandard tier (see Rate Limiting)
Max batch size500 products
Contract versionAPI-Version: 1 on every response

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: 1 is 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/status returns the same value in data.api_version. The older data.version field ("1.0") is kept for compatibility and frozen — it follows neither the contract nor the application; read api_version.
  • A deprecated operation will carry Deprecation, Sunset and Link: <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.
  • PUT with brand_name absent or null detaches the brand; PATCH with brand_name: "" does too; PATCH with brand_name absent or null leaves the brand untouched.
  • POST /ext/products/batch reports data.brands_unresolved: the number of saved products whose brand_name matched 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.

PlanProduct cap
Free2 000
Starter10 000
Growth50 000
Scale100 000
EnterpriseUnlimited

A license that carries a negotiated cap keeps it. At the cap:

  • POST /ext/products answers 403 with error_code BILLING_LICENSE_QUOTA_EXCEEDED — nothing is written. If another creation was checking the cap at the same moment, it answers 409 BILLING_LICENSE_QUOTA_BUSY instead: retry the request.
  • POST /ext/products/batch refuses only the products to be created beyond the cap, each with a named reason in error_details; the other products of the batch (updates included) are processed.

GET/ext/status

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

GET
/ext/status
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": []
}

GET/ext/products

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

GET
/ext/products
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": []
}

GET/ext/products/delta

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).

Request

GET
/ext/products/delta
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/ext/products/{ean}

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

GET
/ext/products/{ean}
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": []
}

POST/ext/products

Create or upsert

Creates a new product or updates an existing one, matched by EAN.

  • EAN not found → creates the product (name is 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

Statuserror_codeWhen
422VALIDATION_URL_SLUG_TOO_LONGurl_slug longer than 255 characters
422VALIDATION_URL_SLUG_SCHEMEurl_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
403BILLING_LICENSE_QUOTA_EXCEEDEDCreation refused: the plan's product cap is reached
409BILLING_LICENSE_QUOTA_BUSYAnother 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, or archived.

  • 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

POST
/ext/products
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": []
}

POST/ext/products/batch

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_slug longer than 255 characters;
  • a weight, width, height or depth out 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 (&#58;, &colon;, 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

POST
/ext/products/batch
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"] }
  ]
}

PUT/ext/products/{ean}

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

PUT
/ext/products/{ean}
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": []
}

PATCH/ext/products/{ean}

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

PATCH
/ext/products/{ean}
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).

FieldTypeNotes
eanstringEAN-13 or equivalent — required on POST
namestringProduct title — required when creating via POST
brand_namestringMatched against the brand directory, never created — see Brands
descriptionstringLong description (HTML allowed)
short_descriptionstringShort marketing text
referencestringManufacturer reference
supplier_codestringImport 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
asinstringAmazon ASIN
upcstringUniversal Product Code
gtinstringGlobal Trade Item Number
statusstringactive · inactive · archived
is_activebooleanShorthand for status = active
weightnumberWeight in kg — lower than 10 000 000; NaN and infinity refused
widthnumberWidth in cm — lower than 100 000 000
heightnumberHeight in cm — lower than 100 000 000
depthnumberDepth in cm — lower than 100 000 000
unit_of_measurestringunit, pack, m, kg, …
external_idstringYour system's internal ID
url_slugstringSEO 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.

FieldTypeNotes
skustringInternal reference of the product, unique per tenant — neither reference (manufacturer) nor a supplier reference
conditionstringnew · refurbished · used; null when not set
net_contentnumberNet content, for the unit price
net_content_unitstringml · cl · l · g · kg · cm · m · m2 · m3 · unite
gross_weightnumberGross weight in kg, packaging included (weight is the net weight)
fabricantobjectProduct 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_securiteobjectSafety warnings per ISO 639-1 language: { "fr": "…", "en": "…" }

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

GET
/api-keys
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

    true for a pm_test_ key, false (default) for a pm_live_ production key.

Request

POST
/api-keys
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

GET
/api-keys/{key_id}
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

PATCH
/api-keys/{key_id}
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

DELETE
/api-keys/{key_id}
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_ and pm_live_ prefix on rotation.

Request

POST
/api-keys/{key_id}/rotate
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"
}

Was this page helpful?