API ERPJS
API (Application Programming Interface) is a set of tools for automating work with the ERPJS system. It allows you to create, read, update, and delete records in any register, run reports, work with files, and receive real-time updates via WebSocket.
Address
All paths below are relative to your system's address — the one you open ERPJS at in the browser (of the form https://s.erpjs.biz/your-system). There is no separate prefix such as /v1.
Authentication
Obtaining a Token
A JWT token is required to work with the API. Signing in issues it:
curl -X POST https://s.erpjs.biz/your-system/public/login \
-H "Content-Type: application/json" \
-d '{"login": "your_login", "password": "your_password"}'
jwt field — this is the token for subsequent requests. The company is not passed in the request: the server determines it and puts it into the token. The token is valid for 8 hours.
Instead of a password you can sign in with the user's MCP token (created by the "Generate MCP token" item in the user card): POST /public/mcp-login with the body {"token": "<MCP token>"} returns the same kind of jwt.
Every sign-in ends the user's previous sessions. An integration and a person under the same login knock each other out — create a separate user for the integration. The "API token" in the erpjs.biz cabinet belongs to a different service and does not work for the system's
/records/*.
Passing the Token
The token is passed in an HTTP header: Authorization: Bearer <token>. The server does not read cookies to check the token.
Example
export TOKEN="<your_token>"
export BASE_URL="https://s.erpjs.biz/your-system"
curl -X POST $BASE_URL/records/list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"datadef": "Contacts", "query": {"limit": 10}}'
Working with Records
All record operations are performed via POST requests to /records/* endpoints.
Record List
POST /records/list — retrieve records from a register with filtering, sorting, and pagination.
{
"datadef": "Invoices",
"query": {
"where": { "fOKFlag": 1 },
"order": [["fInvDate", "DESC"]],
"limit": 50,
"offset": 0,
"search": "search text"
}
}
The response is an array of records (without a total count). Only the keys where, order, limit, offset, search are allowed in query. The server returns no more than 10,000 records per request — beyond that the answer is 400; page through with limit and offset.
POST /records/sqlList — extended list with specific field selection and grouping (query.groupBy). The fields list is required.
{
"datadef": "Invoices",
"fields": ["fSerNr", "fCustCode", "fSum4", "fInvDate"],
"query": {
"where": { "fOKFlag": 1, "fInvDate": { "$gte": "2026-01-01" } },
"order": [["fInvDate", "DESC"]],
"limit": 100,
"offset": 0,
"search": { "search": "Customer", "column": "fCustCode" }
}
}
Response: {"res": [...], "qty": "1234"} — an array of records and the total count (qty comes as a string).
Give the where condition as an object: values are compared as they are, operators carry $ ($in, $gte, $like and so on); bare in or gt is a 400 error. The server still accepts an SQL string too, but that is a deprecated form: it may be switched off, and it must start with the word WHERE.
Viewing a Record
POST /records/show — retrieve full record data, including matrices, files, and links.
{
"datadef": "Invoices",
"id": 123
}
The response contains:
- All record fields
- each matrix as a separate key under its own name, e.g. "InvoicesMatrix": {"headers": [...], "payload": [...]} (item lines in an invoice)
- __files — number of attached files
- __links — number of linked documents
- __lock — present only when the record is locked: true — by your lock, a login — by someone else's
- __lastUpdated — time of last update
- as well as __permissions (rights on the record and its fields) and __revision
Default Values
POST /records/defaults — retrieve default values for a new record.
{
"datadef": "Invoices"
}
Creating a Record
POST /records/create — create a new record. Matrices are passed in the same request.
{
"datadef": "Contacts",
"data": {
"fCode": "C-001",
"fName": "New Company",
"fCUType": 1,
"fPhone": "+380501234567"
}
}
Example of creating a record with a matrix (matrix line fields also start with f):
{
"datadef": "Invoices",
"data": {
"fCustCode": "C-001",
"fInvDate": "2026-03-28",
"fCurncyCode": "UAH",
"InvoicesMatrix": {
"payload": [
{
"fArtCode": "ITEM-001",
"fQuant": 5,
"fPrice": 100
}
]
}
}
}
Updating a Record
POST /records/update — update an existing record.
{
"datadef": "Contacts",
"id": 123,
"data": {
"fPhone": "+380509876543"
}
}
Lock the record first.
updateaccepts only a record you have opened for editing withsetLock(see "Record Locking"); without a lock the answer is 423 "record is not open for editing", and under someone else's lock it is 423 too.Send only the fields you are changing. A field that is absent from
datais treated as untouched and keeps the value stored in the document; a matrix absent fromdatais not touched either. A field you send is written as it is and can overwrite a change someone made between your read and your write. A matrix you send replaces the previous one entirely.
Dates
A date-and-time value comes in the records/show response with the Z label (for example, 2026-09-15T07:05:00Z).
If you send it back unchanged, the system recognises the unchanged value and keeps it as it was (the kernel works this
way since 26.09.2026). A shift can happen only if someone else changed the value between your read and your write — so
the main rule stays: do not send a date you did not change.
The ERPJS MCP connector (the update_record tool) also recognises a date you have read and are sending back
unchanged; with applyWactions: false the body goes to the system verbatim.
A new value:
- in the response form (
2026-09-15T07:05:00Z) is read as a real instant — 10:05 in Kyiv; - with an explicit offset:
2026-09-16T08:45:00+03:00is stored as 08:45 Kyiv time; - as
DD/MM/YYYY("17/09/2026") orYYYY-MM-DD— as a calendar day with no shift; in a date-and-time field the time becomes 00:00. To set the time, useDD/MM/YYYY HH:mmorYYYY-MM-DD HH:mm:ss— this is the server's local time.
Date-only fields (DD/MM/YYYY in the window, and DD/MM/YYYY in the show response too) are not affected by any shift.
Deleting a Record
POST /records/delete — delete a record.
{
"datadef": "Contacts",
"id": 123
}
Note: a record with attached files cannot be deleted. Remove the files first (
softDeleteAttachment).
Metadata and Data Model
POST /system/model — retrieve the structure (schema) of a register: field list, types, validations.
{
"datadef": "Invoices"
}
POST /system/meta — retrieve record metadata: __lock, __files, __links, __actions, __lastUpdated, __revision, __permissions.
{
"datadef": "Invoices",
"id": 123
}
Reports
POST /reports/<Report>/<view> — run a report with parameters. For most reports the view is simple (for example, /reports/SalesReport/simple).
{
"fFrom": "2026-01-01",
"fTo": "2026-03-31",
"fCustCode": "C-001",
"_meta": {
"htmlFast": true
}
}
The _meta parameter sets how the report answers:
- htmlFast: true — the answer comes at once as an HTML string; together with getContext: true — the report data as JSON;
- without htmlFast the answer is only a {key}: the result arrives as WebSocket events, and the files are fetched via GET /system/download-report;
- disableHtml: true, disableXlsx: true — skip building the HTML or the Excel file in such a full report.
The server checks report parameters the same way the report window does: a period whose start is after its end, a code
missing from the reference list, a document number that is not a number. In that case the report is not built, and the
response is a validation error with code 400; the field name is in data.field.
Files
Uploading a File
POST /system/upload — upload a file and attach it to a record. Use multipart/form-data.
Form fields:
- file — the file
- data — JSON with parameters: Register, RecordId, FileRealName, FileSize, CHECKSUM (sha256 in hex)
You need rights on the record's register; the file size limit is 10 MB.
Downloading a File
GET /system/download/<uuid> — download a file by its UUID.
Deleting a File
POST /system/softDeleteAttachment — mark a file as deleted. Body: {"datadef": "Attachments", "id": <file id>}; you need the right to edit the record the file is attached to.
Record Locking
The locking system prevents simultaneous editing of a record by multiple users. A lock is required for records/update.
POST /system/setLock— lock a record for editingPOST /system/checkLock— check whether a record is locked and by whomPOST /system/removeLock— remove the lockGET /system/lockList— list of all active locks
A lock lives for 5 minutes. Calling setLock again as the same user extends it by another 5 minutes — the editing window does this every 10 seconds. If your work with the record takes longer, repeat setLock; a lock that was not removed in time (the integration crashed, the tab was closed) expires by itself.
Access Permissions
The body carries a list of names — without it the answer is 400.
POST /system/getPermissionsRegisters{"registers": [...]}— register permissions (list,read,create,edit,removeand more)POST /system/getPermissionsModules{"modules": [...]}— module permissionsPOST /system/getPermissionsReports{"reports": [...]}— report permissionsPOST /system/getPermissionsMaintenances{"maintenances": [...]}— maintenance procedure permissions
WebSocket (Real-Time)
ERPJS supports WebSocket connections via Socket.IO for receiving real-time updates.
Connection: Socket.IO at the path <system address>/socket.io, with the token in the handshake: auth: {token: "<token>"}.
Server events:
- system_version — server version (on connection)
- push:<login> — the user's unread notifications (the answer to :live:)
- update-tasks — task list update
Client events:
- :live: — activity confirmation and a request for unread notifications
- push:success — confirmation that notifications were received (an array of their ids)
- get:activeUsers — request the list of active users (the answer comes through a callback)
Procedures and Maintenance
POST /maintenances/<name>/<task> — run a maintenance procedure (import, export, bulk operations); for most services the task is first. The server checks the role's rights on the service.
POST /custom/<name> — run a server procedure; only procedures declared in the system become routes.
Public Endpoints (No Token Required)
Some endpoints are available without authentication:
POST /public/login— log in to the systemPOST /public/mcp-login— log in with an MCP tokenGET /public/qrDownloadFiles— download files via QR code/openserver/<route>(GETorPOST) — open API endpoints
Everything under /public and /openserver, sign-in included, is rate-limited: 10 requests per 30 seconds from one IP to one path, after that the answer is 429.
Response Codes
| Code | Description |
|---|---|
| 200 | Successful request |
| 400 | Validation error (details in the response body: message, data) |
| 401 | Invalid or missing token, or the session was replaced by a newer sign-in |
| 403 | Insufficient access permissions |
| 404 | Register or record not found |
| 423 | The record is not locked by you (no setLock) or is held by someone else |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
Integration Tips
-
Pagination: use
limitandoffsetfor large data sets. ThesqlListendpoint returnsqty— the total number of records;listreturns at most 10,000 records per request. -
Matrices: when creating or updating a document with items (invoice, order), pass the matrix in the field with the corresponding name (e.g.,
InvoicesMatrix) as{"payload": [...]}. -
Validation: the server performs business logic validation (recordCheck). Validation errors are returned with code 400; the name of the field with the error is in
data.field. -
Locking: before updating a record, set a lock (
setLock) — without itupdateanswers 423 — and remove it afterwards (removeLock). -
Transactions: record creation and updates are executed within transactions. On error, all changes in the database are rolled back automatically.
-
A separate user: the integration's sign-in logs out a person with the same login, and vice versa.