The data API is the REST interface the xMatix web and mobile clients run on, and you can use it to read and write any record in your organization from your own code. There is no separate integration API with different rules. Every entity, standard or custom, is reached by its logical name through one set of endpoints under your organization's API address. Requests and responses are JSON. Every request runs as the signed-in user, with the full security model applied on the server. Every write goes through the complete save pipeline: business rules, validations, scripts, workflows and approvals run exactly as they do in the product.
Overview
| Area | What it gives you |
|---|---|
| Reading | Pages of records with filter, search, sort and paging; single records with related records; related lists |
| Writing | Create, partial update and delete, plus preview endpoints that show the outcome without saving |
| Actions | The same named actions that buttons run in the product, on one record or many, with job tracking for actions that run in the background |
| Bulk and upsert | Create or update many records in one call; upsert matches existing records by id or external id |
| Delta sync | Records changed or deleted since a cursor, for systems that keep a copy of xMatix data |
| Metadata | The data model itself: entities, fields, views and relations |
When to use it
- Integrations. Use it to push orders, customers or transactions in from another system, or to pull records out on a schedule.
- Mirrors and reporting stores. Use delta sync to keep an external copy current without re-reading whole tables.
- Your own front ends. Use it to build a portal, a kiosk or a tool on top of xMatix data with the same rules as the product.
For a one-off spreadsheet load, use Import data instead. For moving configuration (entities, scripts, workflows) between environments, use the CLI. For an AI assistant working on your data, use the MCP endpoint.
How xMatix handles it
- Address and version. Every route has the form
https://<your-api-host>/api/1.0/.... The version segment must match the API version the server runs, which is1.0. A leadingvis tolerated (v1.0), but a barev1or1is rejected with a 400 that names theversionparameter. - Which organization. The API host decides the organization, the same way it does for the CLI. The token must belong to a user of that organization. Otherwise every call is refused.
- JSON shape. Property names are camelCase (
name,accountId). Enumerations are written as their names, not numbers. Ids are GUIDs. - Same pipeline as the product. A create or update runs defaults, validations, business rules, bound scripts, workflow triggers and approval gates. Calculated and roll-up fields are always computed by the server. If your payload includes a value for one of them, that value is dropped.
- Unknown properties. A property that names no field is ignored without an error. A property that names a field which is switched off (inactive) is rejected, so a value is never silently lost.
- Security on every row. Lists, related lists and delta sync return only records the user may read. Fields the user may not read come back masked. Writes check the entity grant, record security and field permissions.
Prerequisites
- A user account for the caller. For an unattended integration, an administrator creates a dedicated user and gives it a security profile with exactly the entities, fields and actions the integration needs.
- A bearer token. Calls carry an OAuth 2.0 bearer token issued by your organization's sign-in authority, the same identity service that signs users into the product.
- Logical names. Entity and field names in routes and payloads are logical names (
Account, not "Accounts"). Find them in Setup → Design Studio → Entities, with the CLI'smetadata pull, or with the MCP endpoint's describe tool.
Procedure
Step 1 — Read a page of records
curl -H "Authorization: Bearer $TOKEN" \
"https://<your-api-host>/api/1.0/data/Account?pageSize=50&pageNo=1&orderBy=name"
The response wraps the page:
{ "count": 1342, "pageSize": 50, "currentPageNo": 1, "data": [ ... ] }
count is the total across all pages. To walk the whole set, increase pageNo until you have read count records. Use a stable orderBy so the pages do not shift between calls.
Step 2 — Read one record and its related records
GET /api/1.0/data/Account/{id} returns the record, or 404 if it does not exist or you cannot see it. Add ?relations= with relation names to include related records inline. To page through one related list, call GET /api/1.0/data/Account/{id}/{relation}. It takes the same filter, orderBy, ids, pageSize and pageNo parameters as a list.
Step 3 — Create a record
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "name": "Harbor Point Traders", "phone": "+91 98000 00000" }' \
"https://<your-api-host>/api/1.0/data/Account"
A successful create returns 201 Created with the saved record, including its new id, its generated document number if the entity has one, and every default and calculated value. The Location header points at the new record. To see the result without saving, send the same body to POST /api/1.0/data/{entity}/preview.
Step 4 — Change a record
PATCH /api/1.0/data/{entity}/{id} with only the fields you are changing. Fields you leave out keep their values. The response is the saved record. To see the result first, use PATCH /api/1.0/data/{entity}/{id}/preview.
To make sure you do not overwrite someone else's newer change, send the record's _ts value from your last read in an If-Match header. If the record has changed since then, the update is refused with 410 and code OBJECT_MODIFIED. Read the record again, reapply your change and retry.
Step 5 — Run an action
POST /api/1.0/data/{entity}/{id}/{action}
{ "entity": { "remarks": "Approved by phone" }, "args": { } }
Both parts of the body are optional. entity is a partial update applied before the action runs. args carries any input the action asks for. The action name is the action's name in the entity's metadata. These are the same actions the buttons on a record run.
- An action that runs immediately returns the updated record.
- An action set to run in the background returns a job handle:
jobId,jobStatus(Pending),entityName,recordIdandactionName. Follow it withGET /api/1.0/data/{entity}/{id}/job, and read the record again once the job has finished. If the latest job failed,POST /api/1.0/data/{entity}/{id}/job/retryruns it again. - If you press the same action again while its job is still running, you get the running job's handle back with
duplicate: true. Nothing new is queued.
To see what an action would do without saving anything, call POST /api/1.0/data/{entity}/{id}/{action}/dry-run with the same body. Where action previews are available to your organization, the dry run runs the action as it really would and then rolls everything back. It reports whether the action would succeed, which fields would change and which messages would show. Nothing is saved or sent.
Step 6 — Delete a record
DELETE /api/1.0/data/{entity}/{id} returns 204 when the record is deleted and 404 when it does not exist. Deleting is normally a soft delete. The record leaves lists and lookups and can be restored from the Recycle Bin until its purge deadline (delete, recycle bin and restore). A record that other records still point to is refused with 409 and code REFERENCE_CONSTRAINT_VIOLATION.
Step 7 — Keep a copy in step with delta sync
- Call
GET /api/1.0/sync/{entity}withoutsinceto start from the beginning. - Store the
nextCursorfrom the response, and keep calling withsince=<nextCursor>whilehasMoreistrue. - Apply each record to your copy. A record with a non-empty
deletedUtchas been deleted, so remove it from your copy. - On later runs, call again with the last stored cursor. You get only what changed since then.
Endpoint reference
Reading
| Endpoint | What it returns |
|---|---|
GET /api/1.0/data/{entity} | A page of records with count, pageSize, currentPageNo and data |
GET /api/1.0/data/{entity}/{id} | One record. ?relations= includes related records inline |
GET /api/1.0/data/{entity}/{id}/{relation} | A page of the records related through a named relation |
GET /api/1.0/data/view/{entity}/{viewId} | One page of a saved view's rows with hasMore instead of a total. The view's columns and conditions apply, and your filter and orderBy add to them |
GET /api/1.0/data/view/{entity}/{viewId}/count | The view's row count, capped (10,000 by default). isCapped: true means "at least this many" |
List parameters:
| Parameter | Meaning |
|---|---|
filter | A filter expression over the entity's fields, evaluated in the database. This is the query-filter side of Expressions |
search | A free-text term. On the list endpoint it matches records whose Name contains the term, and it is combined with filter |
orderBy | A sort expression, for example name or createdDateTime desc |
relations | Related records to include inline |
ids | Comma-separated record ids, to fetch a known set in one call |
pageSize, pageNo | Paging. pageSize defaults to 25 and is capped by the server (250 in the standard configuration). A larger value is reduced to the cap without an error, so read the echoed pageSize. pageNo starts at 1 |
Writing
| Endpoint | What it does |
|---|---|
POST /api/1.0/data/{entity} | Creates a record and returns 201 with the record and its location |
PATCH /api/1.0/data/{entity}/{id} | Partial update. Send only the fields you change |
DELETE /api/1.0/data/{entity}/{id} | Deletes the record. Returns 204, or 404 if it is not found |
POST /api/1.0/data/{entity}/preview | Returns the record a create would produce, with defaults, rules and calculations applied, without saving |
PATCH /api/1.0/data/{entity}/{id}/preview | The same, for an update |
Actions and jobs
| Endpoint | What it does |
|---|---|
POST /api/1.0/data/{entity}/{id}/{action} | Runs a named action on one record. The body is { "entity": {...}, "args": ... }, both optional |
POST /api/1.0/data/{entity}/{id}/{action}/dry-run | Shows what the action would do, then rolls everything back |
GET /api/1.0/data/{entity}/{id}/{action}/prefill | The values the action would preset on the record |
POST /api/1.0/data/bulk/{entity}/{action} | Runs an action on many records. The body is { "items": [ { "id": "...", "entity": {...}, "args": ... } ] } |
GET /api/1.0/data/{entity}/{id}/job | The record's latest background job. Returns 204 when there is none |
POST /api/1.0/data/{entity}/{id}/job/retry | Runs the record's latest failed job again |
Bulk and upsert
| Endpoint | What it does |
|---|---|
POST /api/1.0/data/bulk/{entity} | Creates every record in a JSON array, all or nothing. One refused record rolls the whole batch back |
PATCH /api/1.0/data/bulk/{entity} | Partially updates every record in a JSON array. Each item must carry its id. Ids that are not found are skipped |
DELETE /api/1.0/data/bulk/{entity}/{ids} | Deletes the listed records. Returns 204 if all were removed and 404 if any were not. Returns 409 if another record still refers to one of them |
POST /api/1.0/data/upsert/{entity} | Saves each row of a JSON array on its own. A row with an id updates that record. A row with no id but an externalId updates the one record with that external id, if exactly one exists. Any other row is created. The response lists rowIndex, status (Created, Updated or Error), id and errorMessage for every row |
The bulk action endpoint answers { "results": [...] } with one entry per item: status is ok, missing (the record was not found) or error with the message. One failed item does not stop the others.
Delta sync
| Endpoint | What it does |
|---|---|
GET /api/1.0/sync/{entity}?since=<cursor> | Records created, changed or deleted after the cursor, oldest first. pageSize defaults to 100. includeTotal=true adds totalCount |
POST /api/1.0/sync/manifest | For a list of { "entity": "...", "since": "..." } pairs, answers in one call whether each entity has anything new (changed) and whether you can read it (accessible) |
A delta page carries data, nextCursor, hasMore, retentionHorizonUtc and epoch:
retentionHorizonUtcis the oldest point for which deletions are still guaranteed to be reported. If your last full refresh is older than this, re-read the entity in full instead of trusting deltas.epochchanges when the entity's storage is restructured. When it differs from the value you stored, re-read the entity in full.
Metadata
The model itself (entities, fields, views, relations) is available under /api/1.0/metadata/.... This is the surface the CLI's metadata commands use. To discover what fields an entity has, use these routes or the MCP describe tool rather than guessing from record payloads. The same routes also accept changes. Make model changes through the CLI or the setup screens, which validate and track them, rather than by calling these routes yourself.
Business rules
- Validation. A record that breaks a validation or a required field is refused with 422. The
detailslist names each problem withmessage,field,fieldLabelandcode. A business rule that blocks the save is also refused with 422, with codeBUSINESS_RULE_VIOLATIONand the block reasons. - Duplicates. A value that must be unique and already exists is refused with 409 and code
DUPLICATE_VALUE. - Edit lock. While another user has the record open for editing in the product, updates and deletes are refused with 409 and code
record_locked. The message names the person editing. Retry once they have finished. - Approval lock. A record waiting on an approval request cannot be updated or deleted. The API answers 403 with code
RECORD_APPROVAL_LOCKED. - Background jobs. While a background job is processing a document, other actions and new lines on it are refused with 409 and code
record_processing. - Retries without duplicates. On a single create or a single action, send a unique
X-Mutation-Idheader (up to 64 characters) and reuse it on every retry of that same call. If the first attempt already succeeded, the retry returns the existing record (for an action, the record withduplicate: true) instead of doing the work twice. If the first attempt is still running, the retry gets 409 with codemutation_in_flightand aRetry-Afterheader. - Read-only organization. When an organization's subscription is read-only, every write is refused with 403 and error
tenant_readonly. Reads keep working.
Example
A field-service team's billing system creates customer accounts in xMatix every night and reads back the account numbers.
- The integration lists accounts changed since its last run with
GET /api/1.0/sync/Account?since=<stored cursor>. It updates its own records, removes any that carry adeletedUtc, and stores the newnextCursor. - For each new customer, it sends
POST /api/1.0/data/upsert/Accountwith rows such as{ "externalId": "BILL-20417", "name": "Lakeview Dental Clinic", "phone": "+91 98000 11111" }. The first night, the row is Created. On later nights, the same external id finds the existing account and the row is Updated, so re-sending a file never creates duplicates. - One row comes back with status Error and the validation message in
errorMessage. The integration logs that row and carries on with the rest. Every other row in the same call is saved. - When the billing system needs a record action, it calls
POST /api/1.0/data/Account/{id}/{action}with the action's name as it appears in the entity's actions. The integration user's security profile grants that action and nothing else.
Training
Practice exercise
In a sandbox organization, using a test user with read and create rights on Account:
- List the first 10 accounts sorted by name. Note the
count. - Create an account called "Training Account 01" through the preview endpoint, and confirm nothing new appears in the product's account list.
- Create the same account for real. Expected result: 201, a new
id, and the account visible in the product. - Update its phone number with
PATCH, sending the_tsfrom step 3 inIf-Match. Then repeat the same request with the same old_ts. Expected result: the first succeeds, and the second gets 410 OBJECT_MODIFIED. - Delete it, and find it in the Recycle Bin.
Quick reference
- Routes start with
/api/1.0/, not/api/v1/. - Logical names only:
Account,accountId, never on-screen labels. pageSizedefaults to 25 and is capped by the server. Read the echoed value.searchon a list matches Name only. Usefilterfor anything else.- Send
If-Matchwith_tsto avoid overwriting newer changes. - Send
X-Mutation-Idon creates and actions you may retry. - Bulk create is all or nothing. Upsert and bulk actions report per row.
- Background actions return a job. Follow it with
.../{id}/job. - For mirrors, use delta sync and keep
nextCursor.
Permissions
The API applies the calling user's access with no exceptions:
| To do this | The caller needs |
|---|---|
| List, read, related lists, delta sync | Read on the entity. Rows are limited by record security, and unreadable fields are masked |
| Create, bulk create, upsert of new rows | Create on the entity, and create permission on every field sent |
| Update, bulk update, upsert of existing rows | Update on the entity and the record, and update permission on every field sent |
| Delete | Delete on the entity and the record |
| Run an action | Update on the entity and the record, and the grant for that action |
A caller without the access gets 403 with a code such as ENTITY_ACCESS_DENIED, FIELD_ACCESS_DENIED (with the refused fields in deniedFields) or SERVER_ACTION_ACCESS_DENIED. An entity whose product is not enabled for the organization answers PRODUCT_NOT_LICENSED to everyone. Records the caller cannot see simply do not appear, and a single record you cannot see answers 404.
Configuration
Administrators control what an integration can do entirely through the product's own settings. There is no separate API switch.
- Integration user and security profile. Create a dedicated user and assign a security profile under Security profiles. Grant only the entities, operations, fields and actions the integration needs.
- Entity operations. An entity that has an operation switched off (for example, no delete) refuses it through the API with
ENTITY_OPERATION_DISABLED, for every caller. - Actions. Which actions exist, what they are called, and whether they run immediately or in the background are set on the entity's actions in Setup → Design Studio → Entities. A background action returns a job handle instead of the record.
- Validations, business rules and scripts. These apply to API writes exactly as to the product's forms. A rule added for users also constrains every integration. See what runs when you save a record.
- Inactive fields. Switching a field off makes writes that set it fail. Reactivate the field or remove it from the integration's payload.
- Deleted records. Soft-deleted records stay restorable in the Recycle Bin until their Purge After date. See delete, recycle bin and restore.
Common problems
Every call fails with a 400 about the version
The route uses v1 or 1. Use 1.0, as in /api/1.0/data/Account.
Every call fails with 401 or 403 before any data is touched
A 401 not_authenticated means the token is missing or expired, or was issued for a different API address. A 403 tenant_forbidden means the token's user does not belong to the organization of the API host you are calling. Get a token for the right address and the right user. See the developer troubleshooting page.
A 415 naming the entity
The entity name in the route is not a known logical name. Check the spelling and case in Setup → Design Studio → Entities.
A 400 about a filter or orderBy
The expression names a field that does not exist, or a field that is computed and has no stored value to query on. The message names the field. Filter or sort on a stored field instead. Computed fields are still returned in results.
My search returns records I did not expect, or misses some
On the list endpoint, search only looks at Name. To match other fields, write a filter.
I asked for 1,000 records but got fewer
pageSize is capped by the server. The response's pageSize shows the size actually used. Page with pageNo until you have read count records.
A value I sent was not saved
Check whether the property name matches a field's logical name. Unknown names are ignored without an error. Calculated and roll-up fields are always recomputed by the server, so values sent for them are dropped.
A 422 when creating or updating
A validation, a required field or a business rule refused the record. Read details: each entry names the field and the reason. If the code is field_inactive, the field is switched off. Remove it from the payload or ask an administrator to reactivate it.
A 409 when updating or deleting
Read the code. record_locked means another user is editing the record, so retry later. record_processing means a background job is working on the document. DUPLICATE_VALUE means a unique value already exists. REFERENCE_CONSTRAINT_VIOLATION means other records still refer to the one you are deleting.
A 410 OBJECT_MODIFIED
Your If-Match value is older than the record. Read it again, reapply your change and retry.
An action returned a job instead of the record
The action is set to run in the background. Poll GET /api/1.0/data/{entity}/{id}/job until the job finishes, then read the record.
A 429 or 408
The request exceeded the server's per-request budget for time or volume. Ask for smaller pages, split bulk calls into smaller batches, and retry.
Common questions
Do API writes trigger the same automation as the product?
Yes, identically. A record created through POST /api/1.0/data/{entity} goes through the same lifecycle as a form save: defaults, validations, business rules, bound scripts, workflow triggers and approval gates. This cuts both ways. Your integration gets the organization's business logic for free, and a validation that blocks users blocks the integration too. If an integration suddenly starts getting 422 responses, check whether someone added a rule. What runs when you save a record explains the order.
How should an integration page through large tables?
Use pageSize and pageNo with a stable orderBy for one-off reads, and delta sync for anything that keeps a copy continuously. A nightly job that fetches everything re-reads mostly unchanged rows. The same job on sync/{entity} reads only what changed, and the manifest endpoint tells it in one call which entities are worth visiting. Remember that the server caps pageSize, so always read the echoed value.
How do I avoid creating duplicates when a call times out?
Send a unique X-Mutation-Id header on each create or action, and reuse the same value when you retry that call. If the first attempt actually succeeded, the retry returns the record it created instead of creating another. For batch loads, use the upsert endpoint with externalId: re-sending the same rows updates the existing records rather than adding new ones.
Why does a filtered query return records my filter should exclude?
First check the field name. Filters use logical field names, and a misspelt name is refused with a 400 that names it. Then check the value. Picklists compare stored values rather than display labels, and lookups compare ids. If a filter still misbehaves, reproduce it with a small query and compare it against a saved view with the same condition in the product. The query-filter reference covers the rules both use.
Can an integration see more than its user can?
No. The API applies the same security profiles, record security and field permissions as the product. Records the user cannot read never appear, and fields they cannot read come back masked. To widen or narrow what an integration sees, change its user's security profile.
