API for integrations
An API key lets an external system read from and write to INSSACS without anyone's session. This section documents the contract: which endpoints exist, what body each one expects and what it returns.
Keys are created from the panel: API Keys.
Authentication
Two equivalent forms, on every request:
curl https://api.inssacs.com/products \
-H 'x-api-key: ics_a1b2c3d4_e5f6...'
curl https://api.inssacs.com/products \
-H 'Authorization: Bearer ics_a1b2c3d4_e5f6...'
With Authorization: Bearer, the secret must start with ics_; otherwise the server treats it as
a user JWT and answers 401.
| Situation | Response |
|---|---|
| No authentication header | 401 |
| Invalid, revoked or expired secret | 401 Invalid API key |
| Valid key without permission or out of scope | 403 |
Scope: client or account
A key is issued at one of two levels, and everything it does is bound to it:
| Level | Covers | When to pick it |
|---|---|---|
| Client | Every account of the client, including ones created later | A corporate integration consolidating several accounts |
| Account | That account only | A system for one site or subsidiary |
In both cases:
- in lists, client, account and operation filters are ignored when they point outside the scope: the server overwrites them with the key's;
- when reading or writing a specific resource, anything outside answers
403; - when creating, you need not send
clientId: it comes from the key. A client-level key must state which account the new resource belongs to (accountId); an account-level one may omit it.
The key's level shows in the Scope column of the table and in the scope field of responses.
What a key can do
On creation you choose which permissions it holds: by default it gets every permission its creator can grant, and you trim from there (see API Keys). A key has no role: its permission list is everything it can do, and it can be changed later without reissuing it.
With every permission, it covers:
| Resource | Endpoints |
|---|---|
| Account and user tags | Account and operations |
| Operations | Account and operations |
| Products and profiles | Catalogue |
| Consumer users, codes and fingerprints | Users and codes |
| Devices and inventory (planogram) | Devices |
| Deliveries, top-ups, returns, available credit, dashboard, exports, audit | Reports |
What it cannot do
| Blocked | Reason |
|---|---|
Managing API keys (/api-keys) | Excluded on purpose: a key cannot mint another key. |
Creating, editing or deleting clients (POST/PATCH/DELETE /clients) | Only a global operator manages clients (422 clients.actor_not_global). Reading GET /clients does work and returns its own client. |
Controllers and licences (/controllers) | Resource reserved to INSSA. |
Creating or deleting devices (POST/DELETE /devices) | Reserved to INSSA (device_provisioning). The rest of the resource is available. |
Operators and roles (/users, /roles) | Those use cases resolve a real acting user; a key is not one, so they answer 403. |
| Server and lambda errors | Reserved to INSSA. |
/auth/* | That is a human operator's session flow. |
Rate limit
Fixed one-minute window per key: 120 requests by default, or whatever the key has under Rate
limit. Exceeding it returns 429. There is also a global per-IP limit of 300 per minute.
Space out batch jobs and retry with exponential backoff on 429.
Response format
- Success:
200with the resource or list,201on create,202when an export is accepted, and204with no body on activate/deactivate/delete. - Page-based lists (accounts, operations, products, profiles, users):
{ items, total, page, pageSize }, withpagestarting at 1 andpageSizecapped at 1000. - Cursor-based lists (movements):
{ items, nextCursor }. Ask for the next page withcursor=<nextCursor>andlimitcapped at 1000; anullcursor means there is no more. - Amounts: integers in minor units (see Amounts and currency).
- Dates: ISO 8601 in UTC (
2026-08-11T14:03:00.000Z).fromandtoare mandatory on movements.
Error format
application/problem+json, carrying the domain code and — on validations — the field-by-field detail:
{
"type": "https://api.inssacs.com/problems/profiles.device_not_in_account",
"title": "Bad Request",
"status": 400,
"code": "profiles.device_not_in_account",
"detail": "device 665f… not in account",
"traceId": "01J9…"
}
Body validation:
{
"status": 400,
"code": "validation.failed",
"errors": [
{ "field": "cost", "rule": "isInt" },
{ "field": "businessLine", "rule": "isIn" }
]
}
| HTTP code | When |
|---|---|
400 | Invalid body or query (including undeclared fields: the server rejects them, it does not ignore them). |
401 | Key missing, invalid, revoked or expired. |
403 | No permission, out of scope, or reserved resource. |
404 | The resource does not exist or is not visible to the key. |
409 | State conflict: duplicate, export already running, profile with codes. |
422 | Domain rule broken (e.g. invalid fingerprint). |
429 | Rate limit. |
500 | Server error. The traceId is what support needs. |
Per-resource codes are listed in Error messages.
The app publishes Swagger at /docs, but only when not running in production, and it only
describes the JWT authentication scheme. To integrate against production, this manual section is the
reference.