Call ZuloOne from another system
Updated
You are writing a shop, a WMS, or an iPaaS. You create and read records on a ZuloOne tenant over HTTP. You do not edit C# in the workspace. The tenant does not issue your token.
Note
Take the token from login.getzulo.com, then call the tenant. The office SPA still uses
POST /api/auth/login(cookies). That is not the integrator door. Cabinet and control-panel JWTs omitresource, soaudstaysclient_idand the tenant rejects them.
How to#
1. Get a token. aud equals resource. The URL must be on the client's
allow-list.
POST https://login.getzulo.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=integrator
&client_secret=$CLIENT_SECRET
&resource=https://acme.getzulo.com
curl -sS -X POST 'https://login.getzulo.com/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=integrator' \
--data-urlencode "client_secret=$CLIENT_SECRET" \
--data-urlencode 'resource=https://acme.getzulo.com'
Send Authorization: Bearer … on every tenant call. Bearer wins and skips
the office CSRF gate (X-ZuloOne-Client). The email in the token must be an
active MetaUser. An integration account is not Developer.
2. Call a published collection. The public contract is OData:
/odata/{PublicCollectionName}. Empty PublicCollectionName means the
object Name. GET /odata/$metadata lists the collections this tenant
published. If nothing answers, someone on the tenant must
create a data entity first.
The examples below use collection Items and key MetaId — that is one
published projection of the item dictionary, not a platform default. Source
required fields still apply on POST (Name, ItemGroup, UnitOfMeasure).
ItemGroup and UnitOfMeasure are the record GUIDs, not the captions.
ID is issued by the number sequence; do not send it unless the entity maps
it as writable.
GET https://acme.getzulo.com/odata/Items?$filter=Name eq 'Sugar'&$select=ID,Name&$top=20
Authorization: Bearer $ACCESS_TOKEN
curl -sS -G 'https://acme.getzulo.com/odata/Items' \
--data-urlencode "\$filter=Name eq 'Sugar'" \
--data-urlencode '$select=ID,Name' \
--data-urlencode '$top=20' \
-H "Authorization: Bearer $ACCESS_TOKEN"
$filter, $select, $top, $skip, $count=true work on stored fields.
$count=true puts @odata.count next to value. A computed field in
$filter is 400.
Create a row with POST to the collection. Update by key with PATCH.
POST https://acme.getzulo.com/odata/Items
Authorization: Bearer $ACCESS_TOKEN
Content-Type: application/json
{
"Name": "Sugar",
"ItemGroup": "11111111-1111-1111-1111-111111111111",
"UnitOfMeasure": "22222222-2222-2222-2222-222222222222"
}
PATCH https://acme.getzulo.com/odata/Items(aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee)
Authorization: Bearer $ACCESS_TOKEN
Content-Type: application/json
{"Name": "Sugar, white, 1 kg"}
curl -sS -X POST 'https://acme.getzulo.com/odata/Items' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"Name":"Sugar","ItemGroup":"11111111-1111-1111-1111-111111111111","UnitOfMeasure":"22222222-2222-2222-2222-222222222222"}'
curl -sS -X PATCH \
'https://acme.getzulo.com/odata/Items(aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee)' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"Name":"Sugar, white, 1 kg"}'
GET/PATCH/DELETE of one row is /odata/{collection}({key}).
3. Read the status.
| Code | Meaning |
|---|---|
| 401 | Signature or aud is not this instance (ZuloOne:PublicUrl) |
| 403 | Email not in MetaUsers, account blocked, or no Read/Create/Update/Delete |
| 404 | Unknown collection or IsActive=false |
| 405 | IsReadOnly and you called POST/PATCH/DELETE |
| 400 | $filter on a computed field |
RequireExplicitRoles with an empty matrix is otherwise public over HTTP.
Turn the flag on when that must not happen. Source-table RLS still applies.
A script on the tenant using IDataEntityManager does not need the HTTP
grant; you, over HTTP, do.
4. An operation, not a row. Same Bearer, different URL:
POST /api/rest/{Name}. That is a web service
(isEnabled, isDevOnly, requireExplicitRoles), not a collection. The
same MetaUser is used on /odata, /api/rest/{Name}, and
/api/data/{table}. Prefer OData for records. /api/data/{table} is the
internal table shape — not the public contract.
What's not there yet#
There is no inbound webhook you can register so ZuloOne calls you back.
CompletionHandler on an outbound channel is reserved and unread — see
Send an outbound call. Poll OData, or the
tenant enqueues outbound to a named channel.
No GraphQL. Do not treat /api/data/{table} as the public contract.
Where next#
- Create a data entity — publish the collection you will call
- Data Entity — mapping, rights, events (tenant-side)
- Create a web service — an RPC at
/api/rest/{Name} - Auth and API gates — Bearer vs office cookies
- Integrate via REST — which door the model publishes (outbound, OData, RPC)