Documentation
Getting Started
Data Grid
Modeling
Business Rules
Approvals
Users, Roles & Security
Administration
Installation
Migrating from MDS
Architecture

REST API

The REST API lets external applications read and write entity records over HTTP. Access is controlled with API keys. Each key belongs to a service account — an API user — that takes part in the same role and permission system as a human user.

The API Users tab under Access Management, listing each service account with its key prefix, roles, status and call count
The API Users tab under Access Management, listing each service account with its key prefix, roles, status and call count(click to enlarge)

Or watch the whole life of a key — created and shown once, three real calls and their answers, a write refused by the account's own role, the old key answering 401 the moment it is regenerated, and every call answering 503 with the kill switch off:

The Primentra REST API — API keys, permissions and the kill switch

The API is for server-to-server use only. No CORS headers are set, so browser calls from another origin are blocked. Call it from a backend service, a script, or an integration platform.

Enabling and disabling the API

The global kill switch is in Settings → General Settings → API.

  • On (default) — valid API key requests are processed normally
  • Off — every request using X-API-Key returns 503 Service Unavailable, whatever the key

The toggle saves immediately and takes effect at once. Use it to suspend all external access without deleting a key.

Managing API users

Go to Settings → Access Management → API Users.

Each API user is a dedicated identity for one integration. It is subject to the same roles and permissions as a regular user, so assign only the roles the integration needs.

The How API users work panel at the top of that screen sets out the three steps and stays folded away once you have read it.

Notes

Every API user carries a note — 500 characters, counted down as you type. Use it for what the account is for, who runs it, and which system calls it. It is shown under the name in the list and survives regenerating the key, which is the moment you are most likely to want it.

Create an API user:

  1. Click New API user.
  2. Enter a display name. This field is required.
  3. Optionally name the key. A label helps when you manage several.
  4. Assign one or more roles.
  5. Optionally set a key expiry date. Leave it blank for a key that never expires.
  6. Click Save.

The key is shown once, immediately after creation. Copy it now — it cannot be retrieved again. Only a SHA-256 hash is stored.

Edit — click a row to change the display name, roles, active status or expiry. The key is not affected.

Regenerate key — the old key is invalidated at once, and any integration using it gets 401 Unauthorized on its next request. The new key is shown once.

Delete — removes the service account. Calls using its key return 401 immediately.

The list shows the key prefix, roles, status (Active, Inactive, Expired or No key), call count, last use and expiry. Every column header sorts.

Authenticating requests

Send the key in the X-API-Key header on every request:

X-API-Key: prm_<your-key>

The format is prm_ followed by a random string. Never send the key as a query parameter or in the body.

Rate limiting

The API accepts 300 requests per minute per key. Requests over the limit return 429 with error code RATE_LIMITED. Callers without a key are limited by client address instead.

Response envelope

Every endpoint is under /api/v1/ and returns the same shape:

{
  "success": true,
  "data": { },
  "pagination": { "page": 1, "pageSize": 100, "total": 542, "totalPages": 6 }
}

Errors return:

{ "success": false, "error": { "code": "PERMISSION_DENIED", "message": "..." } }

Endpoints

Entities

MethodPathDescription
GET/api/v1/entitiesList the entities the API user can read
GET/api/v1/entities/:entityIdEntity metadata and attribute definitions

Records

MethodPathDescription
GET/api/v1/entities/:entityId/recordsList records, paginated
GET/api/v1/entities/:entityId/records/:recordIdOne record
POST/api/v1/entities/:entityId/recordsCreate a record
PUT/api/v1/entities/:entityId/records/:recordIdUpdate a record
DELETE/api/v1/entities/:entityId/records/:recordIdDelete a record
ParameterDefaultMaxDescription
page1Page number
pageSize1001000Records per page
searchFree-text search across code and name

Examples

1. Discover your entities. This gives you the entityId values the other calls need.

curl https://your-primentra-server/api/v1/entities \
  -H "X-API-Key: prm_Ab12XyZwQrstUv34"
{
  "success": true,
  "data": [
    { "id": 3, "name": "Customer", "modelId": 1, "modelName": "CRM" },
    { "id": 7, "name": "Product",  "modelId": 2, "modelName": "Products" }
  ]
}

2. Inspect an entity's attributes. Check the attribute ids and data types before you write.

curl https://your-primentra-server/api/v1/entities/3 \
  -H "X-API-Key: prm_Ab12XyZwQrstUv34"
{
  "success": true,
  "data": {
    "id": 3,
    "name": "Customer",
    "attributes": [
      { "id": 12, "name": "Region", "dataType": "Domain", "isRequired": true },
      { "id": 13, "name": "Tier", "dataType": "Text", "isRequired": false },
      { "id": 14, "name": "IsActive", "dataType": "Boolean", "isRequired": false }
    ]
  }
}

3. Read records.

curl "https://your-primentra-server/api/v1/entities/3/records?pageSize=50" \
  -H "X-API-Key: prm_Ab12XyZwQrstUv34"

curl "https://your-primentra-server/api/v1/entities/3/records?search=Acme" \
  -H "X-API-Key: prm_Ab12XyZwQrstUv34"

Records come back as flat objects. A domain attribute carries its row id, plus a _<FieldName>_display property with the readable label:

{
  "id": 101,
  "code": "CUST-001",
  "name": "Acme Corp",
  "createdAt": "2026-02-01T09:14:00",
  "modifiedAt": "2026-08-10T11:02:00",
  "Region": 5,
  "_Region_display": "{EU} Europe",
  "Tier": "Gold",
  "IsActive": 1
}

4. Create a record. The body needs code or name, or both. Pass attribute values in the values array.

curl -X POST https://your-primentra-server/api/v1/entities/3/records \
  -H "X-API-Key: prm_Ab12XyZwQrstUv34" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "CUST-050",
    "name": "New Customer Ltd",
    "values": [
      { "attributeId": 12, "intValue": 5 },
      { "attributeId": 13, "textValue": "Silver" },
      { "attributeId": 14, "intValue": 1 }
    ]
  }'

5. Update a record. Same body shape. Only the attributes present in values are changed.

curl -X PUT https://your-primentra-server/api/v1/entities/3/records/102 \
  -H "X-API-Key: prm_Ab12XyZwQrstUv34" \
  -H "Content-Type: application/json" \
  -d '{ "code": "CUST-050", "name": "New Customer Ltd",
        "values": [ { "attributeId": 13, "textValue": "Gold" } ] }'

6. Delete a record.

curl -X DELETE https://your-primentra-server/api/v1/entities/3/records/102 \
  -H "X-API-Key: prm_Ab12XyZwQrstUv34"

Values array — field names by data type

Data typeField in valuesExample
TexttextValue"textValue": "Gold"
IntintValue"intValue": 42
DecimaldecimalValue"decimalValue": 9.99
BooleanintValue"intValue": 1 (1 = true, 0 = false)
DateTimedateTimeValue"dateTimeValue": "2026-01-15T00:00:00"
DomainintValue"intValue": 5 — the row id of the referenced record

Reading every page

Follow pagination.totalPages until you have them all:

import requests

BASE = "https://your-primentra-server/api/v1"
HEADERS = {"X-API-Key": "prm_Ab12XyZwQrstUv34"}

page, records = 1, []
while True:
    r = requests.get(f"{BASE}/entities/3/records", headers=HEADERS,
                     params={"page": page, "pageSize": 500}).json()
    records.extend(r["data"])
    if page >= r["pagination"]["totalPages"]:
        break
    page += 1

Error codes

CodeHTTPMeaning
VALIDATION_ERROR400Missing required field or invalid input
INVALID_KEY401Key not found or invalid
KEY_DISABLED401The key or its linked user is inactive
KEY_EXPIRED401The key is past its expiry date
PERMISSION_DENIED403The role lacks the permission for this operation
LICENSE_EXPIRED403The licence or trial has expired. Reads and exports still work; writes are refused until it is renewed
NOT_FOUND404Entity or record does not exist
RATE_LIMITED429More than 300 requests in one minute on this key
API_DISABLED503The global kill switch is off
INTERNAL_ERROR500Unexpected server error

Approval workflows and the API

When an entity has Requires approval enabled, an API write is offered to an approver instead of being applied. Nothing reaches master data until a person decides on it.

The call is not refused. It answers 202 Accepted:

{
  "success": true,
  "status": "pending_approval",
  "approvalRequestId": 41,
  "message": "This entity requires approval, so the record was not applied. It is waiting for an approver as request 41.",
  "data": { "approvalRequestId": 41 }
}

This applies to all three writes — create, update and delete. A queued delete leaves the record in place until the deletion is approved.

Write your integration to expect it. A 200 means the change is live. A 202 means it is waiting, and approvalRequestId is the number to quote if someone asks what happened to it. Treating 202 as success-and-done is the mistake to avoid: the data is not there yet.

If the submission could never be approved, you get a 400 with the reason instead — a code that already exists, or a value that breaks a blocking business rule. Those are checked at submission rather than left for the approver, who did not type the value and cannot fix it.

Why the API is treated like a person and staging is not

Staging is the one way in that still publishes directly on an entity that requires approval. The API is machine-driven too, so it is fair to ask why it does not get the same pass.

The answer is not who is at the keyboard. It is how hard the door is to open.

StagingREST API
What you need to use ita SQL login with write rights on the stg schemaan API key
Who can grant ita database administratorany Primentra administrator, in about ten seconds
Has a user identitynoyes — a service account with roles and permissions
Can be told what happenednoyes, in the HTTP response

If an API key could write past approval, then approval would be optional for anyone able to make a key. The setting would still look switched on and would protect nothing. Reaching the stg schema already needs the kind of access approval was never guarding against.

The second reason is practical. An HTTP caller can be told "waiting for an approver, request 41" and do something with it. A batch that ran at three in the morning has nowhere to receive that sentence, and a nightly load held in a review queue stalls for ever — so the approval would be switched off within a week, which is worse than not having it.

So the rule is one line: every route with a user offers the change to an approver. Staging has no user, so it cannot.

What this means for your data

Write access to the stg schema is approver rights, whatever the org chart says. Grant it that way. If that is not acceptable for an entity, keep that entity out of staging — there is no setting that adds review to a staging batch.

Business rules are a separate matter and are never skipped. They run on staged rows too.

Every API write is written to the audit log whether it was applied or queued, so the change is traceable either way.

Usage tracking

Every authenticated request is counted per key, with a breakdown by operation.

CounterIncremented on
TotalEvery authenticated request
ReadGET
CreatePOST
UpdatePUT
DeleteDELETE

The counters appear as R/C/U/D badges in the API Users table, in the dashboard's API keys widget, and as a total API calls card in the dashboard KPIs. Tracking is fire-and-forget and never slows a response.

─── Technical ───

Database tables

ColumnTableDescription
KeyHashApiKeysSHA-256 hash of the raw key. The raw key is never stored
KeyPrefixApiKeysFirst 12 characters of the key, shown in the UI
CallCountApiKeysIncremented on every authenticated request
LastUsedAtApiKeysTimestamp of the most recent request
ExpiresAtApiKeysNULL means the key never expires
IsDeletedApiKeysSoft delete. The row is kept for audit; the key is rejected
UserTypeUsers'api' for service accounts, 'standard' for people

Authentication flow

  1. The client sends X-API-Key: prm_<key>.
  2. The server hashes the key with SHA-256 and looks the hash up in ApiKeys.
  3. If the key is valid — not deleted, not inactive, not expired, and the API is not globally disabled — the request proceeds as the linked API user.
  4. CallCount is incremented and LastUsedAt updated.
  5. Role-based permission checks run exactly as they do for a human user.

Kill switch caching. The apiEnabled setting is cached in memory for 30 seconds. Toggling it in General Settings clears the cache at once, so the change is immediate. The 30-second window only matters when the setting is changed outside the application.

Ready to get started?

Start managing your master data with Primentra today.

View Pricing
REST API | Integration & API | Docs | Primentra