Overview
The public API lets a third-party system (ERP, e-commerce platform, business tool, script) read and write your company’s data in ApsionScan, machine to machine, without going through the user interface.
- Base URL:
https://apsionscan.com/api/v1 - Format: UTF-8 JSON, ISO 8601 dates in UTC, camelCase properties
- Authentication: one API key per integration, sent in the
Authorizationheader - Versioning in the URL: any breaking change will go through
/api/v2; adding optional fields is not a breaking change
Current status
The API is being built in stages. The authentication layer is in production; business resources arrive in the next releases. This page is updated at every stage (see the date at the top and the history at the bottom).
| Feature | Status |
|---|---|
| API keys, scopes, expiry, revocation, IP allowlist | Available |
GET /v1/me: key identity (connection test) | Available |
Standard error format (RFC 7807), Idempotency-Key header, cursor pagination, per-key rate limit, OpenAPI specification | Available |
| Products, warehouses and locations, stock levels, movements, transfers | Available |
| Customers, customer orders, suppliers, purchase orders and receipts | Planned |
| Key management in Settings › API keys | Available |
| Outgoing webhooks (event notifications) | Planned |
Getting an API key
AvailableA key belongs to a company. It is attached to a technical service account, which does not use a seat and cannot sign in to the app. Only the company’s owner and administrators can create or revoke keys.
- In ApsionScan, open Settings › API keys (tab shown with the “Manage API keys” permission, granted by default to the owner and administrators).
- Click “New key”, give it a name, a source and the permissions it needs, then copy the secret shown.
- The secret is shown only once, at creation. ApsionScan only keeps a hash of it and cannot send it back to you.
- Each key has a name, a source (your integration’s identifier, for example
erp-x) and a list of scopes. - Options: expiry date and a list of allowed IP addresses or CIDR ranges. Name, permissions, IPs and expiry can be changed later; the source cannot.
Authentication
AvailableA key looks like aps_live_<prefix>_<secret>. Send it with every request in the Authorization header, using the Bearer scheme.
curl https://apsionscan.com/api/v1/me \
-H "Authorization: Bearer aps_live_AbCd1234_yourSecretOf32Characters00000000"{
"company": { "id": "7c0b…", "name": "Your company" },
"apiKey": {
"id": "1f3e…",
"name": "ERP production",
"prefix": "aps_live_AbCd1234",
"source": "erp-x",
"scopes": ["products:read", "stock:read"],
"expiresAt": null
},
"rateLimit": { "requestsPerMinute": 600 }
}- Keys are only accepted under
/api/v1. Elsewhere the request is rejected (403api_key_not_allowed). - Conversely,
/api/v1only accepts keys: an app session or a user token is rejected there (401api_key_required). - Only call the API from a server. A key must never appear in a browser, a mobile app or a code repository.
Scopes and permissions
AvailableEvery request must pass two checks: the key’s scope (what the key is allowed to do) and the service account’s permissions within the company (what the company allows it to do). Grant each key the minimum it needs.
| Scope | Grants access to |
|---|---|
products:read · products:write | Product catalogue |
warehouses:read | Warehouses and locations |
stock:read · stock:write | Stock levels, movements and transfers |
orders:read · orders:write | Customers and customer orders |
purchasing:read · purchasing:write | Suppliers, purchase orders and receipts |
webhooks:manage | Webhook subscriptions (coming) |
GET /v1/me requires no scope. Business scopes will be enforced as soon as the matching resources are released.
Errors
AvailableErrors follow RFC 7807 (Content-Type: application/problem+json). Use the stable code field in your logic; detail is human-readable text that may change. requestId (also in the X-Request-Id header) identifies the request for support.
{
"type": "https://apsionscan.com/en/developers/api#erreurs",
"title": "Forbidden",
"status": 403,
"code": "insufficient_scope",
"detail": "Cette clé API n'a pas le(s) scope(s) requis : stock:write.",
"instance": "/api/v1/stock-movements",
"requestId": "6f1c2d9e-…",
"requiredScopes": ["stock:write"]
}| HTTP | code | Meaning |
|---|---|---|
| 401 | invalid_api_key | Unknown or malformed key, or wrong secret |
| 401 | api_key_revoked | Key revoked: use a new key |
| 401 | api_key_expired | Expiry date has passed |
| 401 | api_key_required | /api/v1 route called without an API key |
| 403 | api_key_not_allowed | Key used outside /api/v1 |
| 403 | ip_not_allowed | IP address not in the key’s allowlist |
| 403 | insufficient_scope | The key lacks the required scope (listed in requiredScopes) |
| 403 | forbidden | Insufficient permission for the service account, or the company’s subscription is inactive |
| 400 | validation_failed | Invalid body or parameters: field-by-field detail in errors |
| 400 | idempotency_key_required | Write without an Idempotency-Key header |
| 422 | idempotency_key_reused | Same Idempotency-Key reused for a different request |
| 409 | request_in_progress | A request with the same Idempotency-Key is still running |
| 400 | invalid_cursor | Invalid pagination cursor |
| 400 | invalid_json | Request body is not valid JSON |
| 413 | payload_too_large | Request body over 1 MB: split the request |
| 429 | rate_limited | Key limit reached: wait for the duration given by Retry-After |
Error-specific fields (e.g. requiredScopes, retryAfter) are added to the body, as RFC 7807 allows. The detail text is currently in French.
Security and key lifecycle
Available- Immediate revocation: a revoked key is rejected from the very next request.
- Zero-downtime rotation: a new key can be issued with the same settings while the old one stays active. Switch your integration over, then revoke the old key.
- IP allowlist: when set, only those addresses or ranges can use the key.
- Expiry: a key can have an end date, after which it is rejected.
- Traceability: the date and IP address of the last use are recorded; creation and revocation are written to the company’s activity log.
- One key per integration and per environment: if one leaks, you only revoke that one.
Rate limits
AvailableEach key has its own limit (600 requests per minute by default, reported by GET /v1/me). Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window ends). Beyond that, the API responds 429 rate_limited with Retry-After.
The platform-wide limit also applies: 100 requests per minute per IP address.
Idempotent writes
AvailableEvery write (POST, PUT, PATCH) requires the Idempotency-Key header: a unique identifier per operation (a UUID, for example), 1 to 255 characters. If the network drops, send the same request again with the same key: you get the same response, without a duplicate.
- Same key, same request: the original response is returned, with the
Idempotent-Replayed: trueheader. - Same key, different request: 422
idempotency_key_reused. - Same key while the first request is still running: 409
request_in_progress. - A 4xx error is stored and returned identically; after a 5xx error, you can retry with the same key.
- Keys are kept for 7 days.
POST /api/v1/stock-movements HTTP/1.1
Authorization: Bearer aps_live_…
Idempotency-Key: 4f9a6c1e-2b7d-4c55-9a1e-0c3f8d2b6e71
Content-Type: application/jsonPagination and sync
AvailableLists use cursor pagination: limit (1 to 200, default 50) and cursor. The response contains data and nextCursor; pass nextCursor to the next request until it is null. The order is stable (modification date, then identifier).
For incremental sync, add updatedSince (ISO 8601): only items modified since that date are returned. Store the date of your last run and reuse it on the next one.
{
"data": [ … ],
"nextCursor": "WyIyMDI2LTEwLTE2VDEwOjAwOjAwLjAwMFoiLCJiIl0"
}OpenAPI specification
AvailableThe public API’s OpenAPI specification is available at https://apsionscan.com/api/v1/openapi.json, with an interactive test console at https://apsionscan.com/api/v1/docs. They only describe /api/v1 routes.
Catalog and stock
AvailableYour products can be referred to by your own identifier (externalId), scoped to the key’s source: PUT /v1/products/by-external/{externalId} creates the product if unknown (201, no initial stock) or updates it (200). The SKU is never changed. In a movement or transfer, refer to the product by productId, productExternalId or sku (only one).
| Method | Route | Scope |
|---|---|---|
| GET | /v1/products · /v1/products/{id} · /v1/products/by-external/{externalId} | products:read |
| PUT | /v1/products/by-external/{externalId} | products:write |
| GET | /v1/warehouses · /v1/warehouses/{id}/locations | warehouses:read |
| GET | /v1/stock-levels (filters productId, warehouseId, locationId, lotId) | stock:read |
| POST | /v1/stock-movements (IN, OUT, ADJUSTMENT, TRANSFER) | stock:write |
| GET | /v1/stock-movements | stock:read |
| POST | /v1/stock-transfers · /{id}/ship · /{id}/receive | stock:write |
| GET | /v1/stock-transfers/{id} | stock:read |
curl -X POST https://apsionscan.com/api/v1/stock-movements \
-H "Authorization: Bearer $APSIONSCAN_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"productExternalId":"ERP-VIS-1","type":"IN","quantity":50,"locationId":"<uuid>","unitPrice":0.12,"batchNumber":"L-2026-42"}'Insufficient stock: 422 insufficient_stock. A kit going out creates one outbound movement per component: the data response lists every movement created.
Sales and purchasing
AvailableCustomers, customer orders, suppliers and purchase orders are created or updated by your identifier (PUT …/by-external/{externalId}): 201 on creation, 200 on update. An identical call changes nothing, so you can resend your data at every sync. An order is linked to a customer with customerId or customerExternalId; its contact details are then taken from the customer.
| Method | Route | Scope |
|---|---|---|
| GET | /v1/customers · /v1/customers/{id} · /v1/customers/by-external/{externalId} | orders:read |
| PUT | /v1/customers/by-external/{externalId} | orders:write |
| GET | /v1/customer-orders (filter status) · /{id} · /by-external/{externalId} | orders:read |
| PUT | /v1/customer-orders/by-external/{externalId} | orders:write |
| POST | /v1/customer-orders/{id}/cancel | orders:write |
| GET | /v1/suppliers · /v1/suppliers/{id} · /v1/suppliers/by-external/{externalId} | purchasing:read |
| PUT | /v1/suppliers/by-external/{externalId} | purchasing:write |
| GET | /v1/purchase-orders (filter status) · /{id} · /{id}/receipts · /by-external/{externalId} | purchasing:read |
| PUT | /v1/purchase-orders/by-external/{externalId} | purchasing:write |
| POST | /v1/purchase-orders/{id}/send | purchasing:write |
curl -X PUT https://apsionscan.com/api/v1/customer-orders/by-external/WEB-10045 \
-H "Authorization: Bearer $APSIONSCAN_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"customerExternalId":"CLI-88","lines":[{"sku":"VIS-M6-20","quantity":10}],"confirm":true}'Update rules: a customer order’s header stays editable while it is DRAFT or CONFIRMED, its lines only while it is a draft. A purchase order stays editable while it is DRAFT or SENT with nothing received. Otherwise: 409 order_locked (with orderStatus). Receipts are scanned in ApsionScan and read with GET /v1/purchase-orders/{id}/receipts. Reading a customer order also returns its shipments (carrier, tracking number, quantities, lots) and serial numbers. POST /v1/purchase-orders/{id}/send sends the purchase order to the supplier (email if they have an address); for a key created before 2026-10-18, first tick the “Send a PO” permission of the “Intégration API” role (Settings › Roles).
Kits and scanned codes
AvailableGET /v1/identify?code=… finds the product or kit behind a code read by your device: ApsionScan code, linked code, SKU, EAN/UPC, or GS1 supplier label (GS1-128, DataMatrix, QR). For a GS1 label, the response also contains gs1: GTIN, lot, dates and quantity.
A kit is received or transferred in a single operation: one receipt (or transfer) per component, grouped and cancellable in one go in ApsionScan. Refer to the kit by kitId, kitExternalId or kitSku. Without lines, the kit composition × kitCount is used, at the prices suggested by GET /v1/kits/{kitId}/receive-template. Transfers take the lots expiring first; all or nothing.
| Method | Route | Scope |
|---|---|---|
| GET | /v1/identify?code=… | products:read |
| GET | /v1/kits/{kitId}/receive-template | stock:read |
| POST | /v1/kit-operations/receive · /v1/kit-operations/transfer | stock:write |
| GET | /v1/kit-operations (filter kitId) · /v1/kit-operations/{id} | stock:read |
curl -X POST https://apsionscan.com/api/v1/kit-operations/receive \
-H "Authorization: Bearer $APSIONSCAN_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"kitSku":"PAL-VIS","kitCount":3,"locationId":"<uuid>"}'Insufficient stock for a transfer: 422 insufficient_stock, with missing (component, requested and available quantity). Nothing is moved.
Coming next
Planned- Signed outgoing webhooks to be notified of stock and order status changes.
Change history
Orders: shipments (carrier, tracking number, lots) and serial numbers when reading a customer order; send a purchase order to the supplier (
POST /v1/purchase-orders/{id}/send).Security: request body limited to 1 MB (413
payload_too_large), invalid JSON → 400invalid_json; the activity log shows the API key behind each write (“API — key name”); two simultaneous calls for the same external identifier no longer create a duplicate.Kits and codes: scanned code identification (including GS1 labels), kit receipt and transfer in one operation, kit operation history.
Sales and purchasing: customers, customer orders (create, update, confirm, cancel), suppliers, purchase orders and their receipts, by external identifier.
Catalog and stock: products (read, upsert by external identifier), warehouses and locations, stock levels, movements, transfers between warehouses.
v1 conventions in production: RFC 7807 errors (
application/problem+json),Idempotency-Keyon writes, cursor pagination andupdatedSince, per-key rate limit, public OpenAPI specification (/api/v1/openapi.json).Self-service key management in Settings › API keys (create, edit, rotate, revoke).
First release: API key authentication, scopes,
GET /v1/me, error codes, roadmap.