Skip to content

Integration quickstart

This page shows the shortest reliable path from “I have a Moltaro URL and a user” to reading and changing records through the public API. It is written for developers and for AI agents: every Moltaro workspace describes itself through the same endpoints, so one flow works against any installation — discover the schema first, then act on it.

All examples below were executed against a real Moltaro installation; response bodies are real, trimmed for length. The complete contract is in the API reference and in the raw OpenAPI document (moltaro-public-v1.json, also served by every installation at /openapi/moltaro-public-v1.json).

  • WORKSPACE_API_BASE_URL is the API host of the deployed installation, for example https://ops.example.com; all endpoints live under its /api/workspace/ path. It is not https://moltaro.com.
  • Authentication is a bearer token: Authorization: Bearer <token>. Tokens come from the login endpoint or from an administrator-issued API key (see Service accounts and API keys).
  • JSON responses use an envelope: { "Data": …, "Errors": [], "Warnings": [], "Success": true }. Check Success, then read Data. The exceptions are file downloads (raw file content) and low-level protocol failures such as malformed JSON or an unknown route, which return a plain HTTP problem response instead of the envelope.
  • JSON property names are PascalCase. Enums are numeric in JSON; the OpenAPI schema carries the member names as x-enum-varnames (and, where provided, member descriptions as x-enum-descriptions).
  • User docs say record type and record; the API says entity definition and entity instance. See the terminology bridge.

Unattended integrations and AI agents should not run on a person’s login. The credential handoff is a one-time administrator action in the Moltaro Portal or administrative Web Application interface. Installing or registering the Web Application and provisioning access are not part of an agent’s API workflow.

For a trusted AI agent, the administrator opens Administration > Agent integration, selects Create service user for agent, and confirms the high-privilege handoff. Moltaro creates a new Service user with the Admin and Configurator roles, issues a 90-day API key, and downloads a ZIP containing AGENTS.md, CLAUDE.md, .moltaro/credentials.env, and a nested .moltaro/.gitignore. The key appears only in that download.

Extract the kit into the intended project, delete the ZIP, and keep .moltaro/credentials.env local. Never commit, upload, paste, echo, or print the credentials file or MOLTARO_API_KEY. Other unattended integrations can still use Administration > Users to create a narrowly scoped Service user and API key manually.

An agent receives the prepared guide, URL, and token. If they are missing, it asks the user to complete Agent integration in the Portal or administrative interface. It does not create its own service user, issue a key, change roles, or use administrative Agent integration endpoints to discover an unknown workspace.

A service key is gated by roles only — exactly like a human user. With the runtime roles a key covers the record-centric integration surface; with the Admin or Configurator role it can also use the administrative configuration API, including creating and changing entity definitions. Grant the narrowest role set that covers the integration’s task. The one exception is account self-service (profile, password): service accounts have nothing to manage there, and those endpoints answer them with an explicit error.

The curated administrative surface is documented in the Configuration API reference, starting with entity definition and schema field management.

The generated Markdown files contain the installation’s real base URL, the discovery flow, secret-handling rules, and links to the installation-local OpenAPI documents. This is a completed administrative handoff, not an agent task. The agent-integration routes are administrative endpoints of the configured Moltaro Application, not the Portal. An authorized agent may call the read endpoints after it already knows WORKSPACE_API_BASE_URL and has a key; it must not call the access-kit endpoint to provision or elevate itself.

For retry, concurrency, polling, recovery, and audit behavior, apply Reliable API automation.

GET /api/workspace/context requires no authentication and returns the installation’s locale, time zone, and authoring mode:

Terminal window
curl -s https://ops.example.com/api/workspace/context
{
"Data": {
"DisplayName": "Acme Operations",
"Locale": "en",
"TimeZone": "Etc/UTC",
"RegionCode": "us",
"EnabledFeatures": [],
"GoogleMapsApiKey": null,
"BusinessLogicAuthoringMode": "NetOperationProject"
},
"Errors": [],
"Warnings": [],
"Success": true
}

POST /api/workspace/auth/login exchanges credentials for an access token. (For unattended integrations and agents, prefer a service-account API key and skip this step — API keys are sent the same way, as bearer tokens.)

Terminal window
curl -s -X POST https://ops.example.com/api/workspace/auth/login \
-H "Content-Type: application/json" \
-d '{ "UserNameOrEmail": "owner@acme.test", "Password": "<password>" }'
{
"Data": {
"AccessToken": "<jwt-access-token>",
"UserId": "f4ac8709-c8ac-4404-a30a-7ddbcfcc8115",
"UserName": "owner",
"WorkspaceName": "default",
"Locale": "en",
"TimeZone": "Etc/UTC",
"Roles": ["Admin", "Everyone"],
"Modules": { "Boards": { "Enabled": false } }
},
"Success": true
}

Send the token on every following request: -H "Authorization: Bearer <jwt-access-token>". GET /api/workspace/auth/me returns the same profile for an existing token.

GET /api/workspace/entity-definitions returns every record type the caller may see — the workspace’s data model:

{
"Data": [
{
"Id": "I0ub2IyjFcx4",
"Name": "SupportTicket",
"DisplayNameSingular": "Support ticket",
"DisplayNamePlural": "Support tickets",
"Description": "Demo record type for the integration quickstart.",
"CommentsEnabled": true,
"AttachmentsEnabled": true,
"TagsEnabled": true,
"SearchTargetCount": 0,
"AuditTrailEnabled": true,
"IsReadOnly": false,
"DisabledOperations": 0,
"UsageType": 0
}
],
"Success": true
}

Name is the stable internal name. Instance endpoints accept either the Id or this name as {entityIdOrKey}.

Entity Definition discovery does not imply record access. Every record query and direct read is constrained by the definition’s current Security configuration. Missing, invalid, disabled, or stale root View Permission Assignments fail closed.

Step 4 — Read one definition to learn its schema

Section titled “Step 4 — Read one definition to learn its schema”

GET /api/workspace/entity-definitions/{entityDefinitionId} returns the full definition, including tables and fields. This is what tells an agent which field keys exist, their types, and their rules:

{
"Data": {
"Id": "I0ub2IyjFcx4",
"Name": "SupportTicket",
"Tables": [
{
"Id": "oGp2IzJVF8Sn",
"IsPrimary": true,
"Fields": [
{ "Key": "Title", "FieldType": 10, "RequiredWhen": "TRUE", "StringMaxLength": 200 },
{ "Key": "DueDate", "FieldType": 1 },
{ "Key": "Priority", "FieldType": 9,
"Options": [ { "Key": "low", "Value": "Low" }, { "Key": "medium", "Value": "Medium" }, { "Key": "high", "Value": "High" } ] },
{ "Key": "EstimatedHours", "FieldType": 6, "DecimalPrecision": 6, "DecimalScale": 2 }
]
}
]
},
"Success": true
}

FieldType is the numeric EntityFieldTypeEnum (10 = String, 1 = DateOnly, 2 = TimeOnly, 9 = Select, 6 = Decimal, 13 = Reference, 17 = DateTimeOffset, …). The full list with semantics is in Fields and schema. RequiredWhen: "TRUE" means always required; other values are conditional expressions.

DateTimeOffset values are ISO 8601 timestamps with an explicit Z or numeric offset. For example, "2026-07-27T10:15:42.123456+02:00" is returned as the same instant in UTC: "2026-07-27T08:15:42.123456Z". The API and database preserve microseconds, but not the sender’s original offset. Never send a local timestamp without an offset.

POST /api/workspace/entity/{entityIdOrKey}/instances uses a caller-generated OperationKey, the definition identity and current definition row version, plus a Values map keyed by field key:

Terminal window
curl -s -X POST https://ops.example.com/api/workspace/entity/SupportTicket/instances \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "OperationKey": "154e7dc2-9f61-4ba4-a4ab-e1bd94183dfa",
"EntityDefinitionKey": "SupportTicket",
"ExpectedDefinitionRowVersion": "a244a235-1dc6-4e8b-81cf-2db6b972cd55",
"Values": { "Title": "Printer in hall B is jammed", "DueDate": "2026-07-25",
"Priority": "high", "EstimatedHours": 1.5 },
"ResponsibilityCandidates": [] }'
{
"Data": {
"Kind": "AuthorizedEntityDetail",
"Data": {
"EntityDefinitionId": "pYZqCLZWb0K1",
"EntityInstanceId": "c24878e1ff334648aeb4022e19f6cd96",
"RowVersion": "53cedb7e-dad6-425c-86c1-edd23dacfd59",
"AuthorizedFields": {
"Title": "Printer in hall B is jammed",
"DueDate": "2026-07-25",
"Priority": "high",
"EstimatedHours": 1.5
}
},
"IsIdempotentReplay": false,
"CanViewResult": true
},
"Success": true
}

A successful create always has Data.Kind: "AuthorizedEntityDetail". Its nested Data.EntityInstanceId and Data.RowVersion are the stable values for the next read or write. If the caller cannot read the final record, creation is rolled back and the endpoint returns 403; POST does not return a safe receipt. Preserve OperationKey only for an exact retry after an unknown outcome. An exact retry by the same caller with the same OperationKey and body, within the seven-day receipt retention, returns the original result with IsIdempotentReplay: true instead of creating a second record. The replay still answers 201 Created, so check the flag, not the status code. It returns the snapshot taken at creation, whose RowVersion may be out of date. A caller that can no longer read the created record, or one of its stored fields, gets 403 with moltaro.instances.replay.resultNotViewable and no record id: the record was created, so do not retry with a new key, which would create a second record. Reusing the key with a different body fails with 409 and moltaro.security.idempotency.keyReuse.

POST /api/workspace/entity/{entityIdOrKey}/instances/query supports paging, a nested filter tree, sorting, and reference includes:

For filters and sorts that traverse references, use Target.Path instead of the direct Field property. The Entity Instance Query guide gives complete payloads, limits, reference-ID operand rules, access semantics, and saved/shared-view behavior.

{
"Page": 1,
"PageSize": 25,
"Filter": {
"Operator": 0,
"Conditions": [
{ "Field": { "FieldKey": "Priority" }, "Operator": 0, "Values": [ { "Value": "high" } ] }
]
},
"Sort": [ { "Field": { "FieldKey": "DueDate" }, "Direction": 0 } ]
}

The response is a page: { "Items": [...], "TotalCount": 1, "Page": 1, "PageSize": 25, "Permissions": { "CanCreate": true, … } }. Condition Operator is numeric (0 = Eq, 7 = In, 9 = Contains, 13 = IsNull, …); the full operator list is in the query operation schema. A single record is read with GET .../instances/{instanceId}.

Step 7 — Update with optimistic concurrency

Section titled “Step 7 — Update with optimistic concurrency”

PATCH /api/workspace/entity/{entityIdOrKey}/instances/{instanceId} sends only the fields to change in Patch, the RowVersion you last saw, and the stable identities for this logical operation. Omitted keys stay unchanged; an explicit null clears a value:

Terminal window
curl -s -X PATCH https://ops.example.com/api/workspace/entity/SupportTicket/instances/c24878e1ff334648aeb4022e19f6cd96 \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "OperationKey": "138f4d41-a572-4bd4-86b6-af8c41ca301c",
"EntityDefinitionKey": "SupportTicket",
"EntityInstanceId": "c24878e1ff334648aeb4022e19f6cd96",
"Patch": { "Priority": "medium" },
"RowVersion": "53cedb7e-dad6-425c-86c1-edd23dacfd59" }'

A successful PATCH returns Data.Kind: "AuthorizedEntityDetail" and a new Data.Data.RowVersion when the caller may read the final state. If the write commits but the final state is no longer readable, the endpoint instead returns Data.Kind: "Receipt" with OperationKey, CompletedAt, CallerKnownIdentity (EntityDefinitionId and EntityInstanceId), AuditCorrelationId, IsIdempotentReplay, Success: true, and CanViewResult: false. A receipt deliberately contains no RowVersion or record values. Treat it as proof that the operation committed; obtain a fresh authorized record before another write. The same two HTTP result variants apply to archive and restore. A stale RowVersion fails the request instead of silently overwriting someone else’s change.

A successful PATCH that sends at least one writable stored field (a scalar or a table field; an inverse reference whose membership is unchanged does not count) creates a new record version even when every sent value equals the stored one; unchanged values are written, not skipped. The record gets a new RowVersion, its modified and last-activity stamps advance, and other clients that still hold the previous RowVersion get a conflict on their next write. The save also runs as a real update: save functions and the before-save changes that use a sent field as a trigger run again, update triggers fire, and the record history gets an update entry when the audit trail is enabled. Read-only fields are skipped with a warning and calculated fields are ignored. On a record type without calculated or line-number fields, a PATCH that sends only such fields (or an empty Patch) creates no version, but it is still an accepted update and update triggers run. On a record type with calculated or line-number fields, every accepted PATCH, even an empty one, creates a new version because those values are written again. An exact retry with the same OperationKey and body returns the original result, marked IsIdempotentReplay: true, without writing again; current visibility is rechecked. When the caller can no longer read the record, or it was deleted, the retry answers a Receipt without record values instead. The same holds for archive, restore and delete. Send only the fields that actually change, and send no request when nothing changed.

Failures return the same envelope with Success: false and structured errors. This 400 came from creating a record without the required Title:

{
"Data": null,
"Errors": [
{ "Code": "moltaro.instances.validation.requiredFields",
"Message": "Fill in all required fields and try again.", "Field": null, "Type": 1, "Severity": 2 },
{ "Code": "moltaro.instances.field.required",
"Message": "This field is required.", "Field": "Title", "Type": 1, "Severity": 2 }
],
"Success": false
}

Code is stable and machine-readable; Field names the offending field key or PascalCase request property; Message is localized for display. Agents should branch on Code and Field, not on Message. A malformed or incomplete create, update, or lifecycle request answers one error in a fixed order; see request errors. The error shape is documented in Errors and responses, and a curated catalog of core entity and record-write codes is in the error code reference. For create, update, archive, restore, delete and the composite mutation, read GET /api/workspace/error-catalog from the installation you call: it gives each code’s HTTP status, Field and safe reaction for that build, and says how to treat a code it does not list.

Field typeWrite value
Yes/Notrue / false
Date / Time"2026-07-25" / "14:30:00"
String / TextJSON string
Whole numberJSON number
DecimalJSON number or invariant decimal string on write; canonical invariant string padded to configured scale in Entity Instance field envelopes and form-evaluation patches
Selectoption key string; array of keys when the field allows multiple
ClassifierCatalog node id string from the Classifier Catalog bound to the field
User / Role / File / Referenceid string of the target
Inverse referencearray of source record ids
Money{ "Amount": 10.5, "CurrencyCode": "EUR" } — amounts come back as strings
Addressobject with FullAddress, street/locality/region/postal/country parts, optional Latitude/Longitude
Tablemanaged through table-row payloads, not a scalar value

Details per type: Fields and schema.

Workspaces can publish their own C# business logic as callable functions. Which keys exist is workspace-specific — publications are part of the workspace configuration, so discover them per installation. Accounts with the Admin role can list every function and its publications through the Function Catalog in the Configuration API (GET /api/workspace/admin/function-catalog); other integrations receive their callable keys from the workspace administrator.

A command runs synchronously and returns its result in the response. POST /api/workspace/commands/{functionKey} takes optional Args (JSON matching the command’s argument type) and an optional CorrelationId; an empty body is allowed for commands without arguments:

Terminal window
curl -s -X POST https://ops.example.com/api/workspace/commands/close-overdue-tickets \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "Args": { "GraceDays": 3 }, "CorrelationId": "req-8842" }'
{
"Data": {
"RunId": "b6f3e0d8a3c14f7f9a1de2c07f6b4a51",
"Message": "Closed 4 overdue tickets.",
"Result": { "ClosedCount": 4 },
"DurationMs": 118,
"CorrelationId": "req-8842"
},
"Success": true
}

Each publication either allows any authenticated user or requires a specific permission; callers without it get 403. Unknown, disabled, and retired keys all return the same 404 (moltaro.functionCommands.notFound), so command keys cannot be probed.

Long-running logic is published for asynchronous execution instead. POST /api/workspace/functions/{functionKey}/enqueue accepts optional Args, a DeduplicationKey (reuses an active queued job), RunAfter (earliest execution time), and CorrelationId, and returns the queued job:

{
"Data": {
"JobId": "0d2f6c1e58b747a2a9c3f1b4e6d80a97",
"FunctionKey": "rebuild-reporting-snapshots",
"Status": 0,
"CreatedAt": "2026-07-22T09:14:03+00:00",
"NextRunAt": null,
"AttemptCount": 0,
"DeduplicationKey": null,
"CorrelationId": null,
"LastError": null,
"CanCancel": true,
"Run": null
},
"Success": true
}

Poll GET /api/workspace/functions/jobs/{jobId} until Status reaches a terminal value (0 = Queued, 1 = Leased, 2 = Completed, 3 = Failed, 4 = Cancelled). A finished job carries its latest execution under Run, including the structured ResultData payload and a bounded ErrorSummary on failure. While CanCancel is true, POST /api/workspace/functions/jobs/{jobId}/cancel cancels the queued job. Job visibility matches invocability: the initiator and every holder of the publication permission can read and cancel the job.

The full contract — publication and permission model, request options, job lifecycle, and error codes — is in Commands and API functions.