Developers · API v1

ApsionScan Public API

Connect your ERP, online store or scripts to ApsionScan. This page describes the current state of the API, how to authenticate, and what comes next.

Last updated: Published:

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 Authorization header
  • 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).

FeatureStatus
API keys, scopes, expiry, revocation, IP allowlistAvailable
GET /v1/me: key identity (connection test)Available
Standard error format (RFC 7807), Idempotency-Key header, cursor pagination, per-key rate limit, OpenAPI specificationAvailable
Products, warehouses and locations, stock levels, movements, transfersAvailable
Customers, customer orders, suppliers, purchase orders and receiptsPlanned
Key management in Settings › API keysAvailable
Outgoing webhooks (event notifications)Planned

Getting an API key

Available

A 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

Available

A key looks like aps_live_<prefix>_<secret>. Send it with every request in the Authorization header, using the Bearer scheme.

Test your keybash
curl https://apsionscan.com/api/v1/me \
  -H "Authorization: Bearer aps_live_AbCd1234_yourSecretOf32Characters00000000"
200 responsejson
{
  "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 (403 api_key_not_allowed).
  • Conversely, /api/v1 only accepts keys: an app session or a user token is rejected there (401 api_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

Available

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

ScopeGrants access to
products:read · products:writeProduct catalogue
warehouses:readWarehouses and locations
stock:read · stock:writeStock levels, movements and transfers
orders:read · orders:writeCustomers and customer orders
purchasing:read · purchasing:writeSuppliers, purchase orders and receipts
webhooks:manageWebhook subscriptions (coming)

GET /v1/me requires no scope. Business scopes will be enforced as soon as the matching resources are released.

Errors

Available

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

Example: missing scopejson
{
  "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"]
}
HTTPcodeMeaning
401invalid_api_keyUnknown or malformed key, or wrong secret
401api_key_revokedKey revoked: use a new key
401api_key_expiredExpiry date has passed
401api_key_required/api/v1 route called without an API key
403api_key_not_allowedKey used outside /api/v1
403ip_not_allowedIP address not in the key’s allowlist
403insufficient_scopeThe key lacks the required scope (listed in requiredScopes)
403forbiddenInsufficient permission for the service account, or the company’s subscription is inactive
400validation_failedInvalid body or parameters: field-by-field detail in errors
400idempotency_key_requiredWrite without an Idempotency-Key header
422idempotency_key_reusedSame Idempotency-Key reused for a different request
409request_in_progressA request with the same Idempotency-Key is still running
400invalid_cursorInvalid pagination cursor
400invalid_jsonRequest body is not valid JSON
413payload_too_largeRequest body over 1 MB: split the request
429rate_limitedKey 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

Available

Each 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

Available

Every 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: true header.
  • 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.
Examplehttp
POST /api/v1/stock-movements HTTP/1.1
Authorization: Bearer aps_live_…
Idempotency-Key: 4f9a6c1e-2b7d-4c55-9a1e-0c3f8d2b6e71
Content-Type: application/json

Pagination and sync

Available

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

Paginated responsejson
{
  "data": [ … ],
  "nextCursor": "WyIyMDI2LTEwLTE2VDEwOjAwOjAwLjAwMFoiLCJiIl0"
}

OpenAPI specification

Available

The 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

Available

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

MethodRouteScope
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}/locationswarehouses: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-movementsstock:read
POST/v1/stock-transfers · /{id}/ship · /{id}/receivestock:write
GET/v1/stock-transfers/{id}stock:read
Receive 50 units with a lotbash
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

Available

Customers, 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.

MethodRouteScope
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}/cancelorders: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}/sendpurchasing:write
Create and confirm a customer orderbash
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

Available

GET /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.

MethodRouteScope
GET/v1/identify?code=…products:read
GET/v1/kits/{kitId}/receive-templatestock:read
POST/v1/kit-operations/receive · /v1/kit-operations/transferstock:write
GET/v1/kit-operations (filter kitId) · /v1/kit-operations/{id}stock:read
Receive 3 pallets of a kitbash
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 → 400 invalid_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-Key on writes, cursor pagination and updatedSince, 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.

Planning an integration?

We support every new integration personally. Tell us what you need.