For your AI agenthttps://lotics.ai/docs/api.md

REST API

The Lotics API gives you programmatic access to everything in your workspace. Create records, query data, trigger workflows and generate documents – all through a standard REST interface with an OpenAPI 3.1.0 specification.


Overview

  • Protocol: Standard REST. Resources are nouns, HTTP methods are verbs, responses use standard HTTP status codes.
  • Specification: OpenAPI 3.1.0, published at https://lotics.ai/openapi.json (and at https://api.lotics.ai/v1/openapi.json).
  • Base URL: https://api.lotics.ai/v1
  • Content type: All requests and responses use application/json.
  • Date format: ISO 8601 strings in UTC (e.g., 2026-04-04T12:00:00.000Z).
  • Record field values: Follow the same types as the Lotics interface – text, number, date, select, multi-select, linked records, files, and computed fields (formula, rollup, lookup).

The API is the same interface the Lotics web application uses internally. What a key reaches through it is your data and the things that shape it – tables, fields, records, views, workflows, apps, document templates, files, comments and search. Running the organization is not part of that: the routes that manage people, sharing, workspaces, billing and the access log answer a key with its own access 403 whatever its settings, and need an admin signed in. See What a key is allowed to do.


Authentication

API requests are authenticated with organization-scoped API keys.

A key an admin creates in Settings has its own name and its own access. It belongs to nobody: it keeps working when the admin who created it leaves, and every write it makes is recorded under the key’s name rather than under theirs.

A key can also be created for a person, in which case it carries that person’s access — the key list shows Acts as and their name for one — and it stops working when that person is removed from the organization.

Property Detail
Format ltk_ followed by 48 characters (e.g., ltk_vAJZYFb9WrF94Z3OjpdZgxjc...)
Scope Single organization
Who can create Admin role only
Where to create Settings -> API keys
Shown Once, at creation. Copy it then — it cannot be read back afterwards.

The four things you set when you create a key

Field What it decides
Name What the key is called. Its writes carry it, so name the system it serves – “Warehouse sync”, “CI pipeline”.
Access What it reaches: All apps and tables, or Only selected ones – and for the second, which ones. Change it later on the key’s own screen.
Can What it may do – read data, write data, read structure, change structure. See What a key is allowed to do.
Expires Never, on a fixed date, or after a chosen number of days without being used — in which case each use pushes the date out, until one year after the key was created. Lotics emails the organization’s admins 14 days and 3 days before a fixed date or that one-year limit arrives.

Giving a selected key access

You choose the apps and tables on the key itself. Pick Only selected ones and a list appears: every app and table in the organization, whichever workspace it sits in. Pick the ones the integration touches and set what it may do with each — Can view or Can edit for a table, User or Manager for an app.

The same list is on the key’s own screen afterwards, so you can add one, take one back, or change a level at any time. The form asks for at least one, because a key that reaches nothing authenticates and is then refused everywhere. A key can still end up there — its last table archived, or taken away from that table’s Share dialog — and its screen then says so; add an app or a table, or delete the key.

Under the list, Owns names what the key itself created. It reaches those whatever the list says, and the only way to take one away is to transfer it from that app’s or table’s Share dialog. Switching a key to All apps and tables drops its list; switching it back starts from an empty one.

The Share dialog on an app or a table SHOWS a key that reaches it, and lets you take that access away from there. It does not add one and does not change a level — what a key reaches is one decision, made in one place, where you can see the whole of it.

All apps and tables needs no list: the key reaches everything in the organization.

What a key creates, it owns

An app, a table or a record created through a key belongs to the key, not to the admin who created the key — which is what lets an integration outlive whoever set it up. Admins see and manage everything a key owns — except what is inside its knowledge documents and templates, which an admin opens only once it is shared with them — and can transfer any of it to a person at any time.

An app runs with its owner’s access, so an app built by a key set to Only selected ones reaches only what that key reaches.

Sending the key

Include the key in the Authorization header on every request:

Authorization: Bearer ltk_your_key_here

A whole request, ready to run once you swap in your own key and table id — reading a table’s records, which is a POST because the filter, sort and paging travel in the body:

curl -X POST https://api.lotics.ai/v1/tables/tbl_7Qm2xR9kLpTd/records/query \
  -H "Authorization: Bearer ltk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"limit": 10}'

Add -H "x-workspace-id: wsp_3nKpQ8vTzRdW" when the organization has more than one workspace; with a single workspace it is inferred.

What a key is allowed to do

A key’s Can setting says what it may do, separately from what it may reach. Set it when you create the key, and change it later from Settings -> API keys; every change is written to the access log.

Can The key may
Everything Anything its Access reaches, within the four below
Read structure Read tables, fields, views, and the definitions of apps and workflows
Change structure Create or change those definitions
Read data Read records, files, knowledge and comments, and search
Write data Create, update and delete records and files

Pick Only what I choose to combine the four. A key that may do nothing is not a setting — disable the key instead, which is reversible and shows in the access log.

These settings only ever narrow. A key can never do more than its Access reaches, so a key given one table is confined to that table whatever you tick, and tightening a key takes effect immediately.

No key with its own access can run the organization. Whatever its Access and whatever its Can setting, such a key cannot manage people (members, invitations, groups, passwords), change what is shared or who owns it, create or delete workspaces, change workspace settings, set credit limits, read the access log, or publish an app’s API. Those need an admin signed in — in Lotics, in a terminal signed in with lotics auth login, or through a connector. A key that tries is answered with 403 and a message saying so. A key created for a person is that person: it does whatever they are allowed to do.

Replacing a key, and ending one

Three controls on the key’s own screen, from the mildest to the last.

What it does When to use it
Roll key Issues a new secret for the same key. The old secret keeps working for 24 hours, then stops. The name, the Access, the Can setting and everything the key reaches are unchanged. A planned rotation, or a secret you want replaced without an outage. Both keys stand in the list until the old one lapses, and the old one says it was replaced and when it stops.
Enabled The switch on the key’s own screen. Turn it off and press Save: the key stops authenticating at once. Turn it back on and press Save and it works again, with the same secret. A leak, or a pause. Whatever is using the key stops the moment you save, and starts again when you turn it back on.
Delete key Removes the key. It stops working immediately, leaves the list, and no longer appears on the apps and tables it was given. What it created stays, for an admin to transfer. This cannot be undone. A key you are finished with. To stop one for a while, turn Enabled off instead.

Turning a key off does not change its secret, so whoever holds the old one is back in the moment it goes on again. When the secret itself is the problem, roll it — or delete the key.

Roll key is offered on a key’s current secret: one that carries its own access, while it is enabled and has not expired. A key that is turned off, or that has expired, is not rolled at all — create a new key. The older key a roll leaves behind has no secret left to replace either, so its row says it was replaced and when it stops, and the one to roll next is the newest. A key created for a person carries that person’s access, so there is no second secret to issue for it — create a key with the Access you want, then turn the old one off.

A credential somebody signs in as goes one way. A command-line sign-in, and a key created to carry a person’s access, can be turned off or deleted but never turned back on — coming back on would be a sign-in nobody performed. Once it is off, its screen says so and offers no switch: sign in again with lotics auth login, or create a new API key.

Signing in from the command line

The lotics CLI takes either kind of credential, and which one you hold decides what signing out does.

lotics auth login An API key
Who it is You — your own sign-in, for your own terminal Itself, with its own name and its own access
Who can set it up You, from your own terminal An admin, in Settings -> API keys
Expires After 90 days without use; each use pushes that out When the admin set it to, if at all
lotics auth logout Ends it — on this machine and on Lotics Removes it from this machine only. It stays active elsewhere until an admin turns it off.

Use a sign-in for your own terminal. Use a key for a server, a scheduled job, or anything that has to keep running when you are not there.

Error responses

Status Meaning
401 Unauthorized The credential is missing, unrecognized, turned off, ended or expired, or the access behind it is no longer active. The message says which, and what to do about it — sign in again, or ask an admin for a new key. An unrecognized key gets one general answer, so that guessing keys reveals nothing.
403 Forbidden Authenticated, but not allowed to do this — or the organization has been deleted

Keys and terminals

Settings -> Security lists every credential that acts as you — command-line sign-ins and any key created for you — with what it is called and when it was last used. End anything you do not recognize: whatever is using it stops working at once, it never works again, and it leaves the list. lotics auth logout does the same to the terminal you are on. Nothing is erased — what that credential did stays in the access log, which is where the history of who had access is read.

Admins see every key in the organization at Settings -> API keys, with who created each one.

When someone leaves the organization, every credential that acts as them ends with them — their own sign-ins and any key that acts as them — and restoring them later does not bring those back: they sign in again. A key with its own access is not one of them: it is not that person, so the integrations running on it keep running.

Security best practices

  • Keys are shown once at creation. Copy and store them securely (e.g., environment variables, secrets manager).
  • Give each key a descriptive name (e.g., “Production sync”, “CI/CD pipeline”) for easy identification.
  • Give each key only the Access it needs — Only selected ones, holding just the apps and tables that integration touches, is what bounds a leak.
  • Give each key only the Can setting it needs — a key that only reads cannot be made to write.
  • Rotate on a schedule with Roll key: the new secret is live immediately and the old one runs for another 24 hours, so nothing goes down while you deploy it.
  • If a key is compromised, open it at Settings -> API keys, turn Enabled off and press Save — it stops at once — then create a new key and delete the old one.
  • Use separate keys for different environments (production, staging, development).

Available endpoints

The API provides full CRUD operations for all primary entities, plus specialized operations like record aggregation, document generation, and global search.

Resource Operations Notes
Tables List, Create, Get, Update, Delete, Clone Includes field definitions. Clone duplicates structure and optionally data.
Fields Create, Update, Delete Add or modify fields on existing tables. Supports all field types including computed fields (formula, rollup, lookup).
Records Query, Get, Get by IDs, Create, Update, Delete, Aggregate Query supports filters, sorts, cursor pagination. Aggregate returns count, sum, avg grouped by field. Update can append to or remove from a multi-value field instead of replacing it.
Views List, Create, Get, Update, Delete Views store filter, sort, field visibility, and color rule configurations.
Workflows List, Create, Get, Update, Delete Includes trigger configuration, step definitions, and execution history.
Document Templates List, Create, Get, Update, Delete, Generate Generate fills a template with record data and produces a PDF or Excel file.
Apps List, Create, Get, Update, Delete Apps are interactive interfaces built on top of tables.
Comments List, Create, Update, Delete Comments are attached to records. List supports filtering by record.
Files Upload, Read, Delete Upload files to attach to file fields on records. Read returns signed download URLs.
Search Global search Search across all tables and records in the organization.

How a number reads: notation

A number field, and a formula yielding a number, states how its figures read as one notation: { "style": "decimal" }, money { "style": "currency", "currency": "USD" }, or a quantity { "style": "unit", "unit": "kg" } — "percent" is a unit, and 10 reads 10%. A currency or a unit read on each row from a single select of that row is { "per_row": "fld_…" } in its place. A rollup’s and a lookup’s notation is derived, and returned on the field.

The keys notation replaced — format, currency, unit, unit_field and currency_field, and a formula’s format: "link" (now link: true) — are no longer read. A write that states them naming another reading than the one it leaves the field in is refused, and the error names the notation to send: "format": "currency", "currency": "USD" on a new field is refused, while "format": "number" on a new field, or the keys sent back as the field holds them, are taken. To change how a field reads, send notation; sending the field back as you read it, with only notation changed, works. The old keys are still returned beside notation for now — read notation. A currency must be an ISO 4217 code wherever a write changes it; a field already holding another spelling keeps it through an edit that leaves the notation alone.

Updating a multi-value field

A record update sends only the fields you name, and for a multi-value field — attachments, a multi-select, linked records, assigned people — you can name the ITEMS rather than the whole list. add_to appends the items you give, remove_from drops them, and both resolve against the record as it stands when the write lands.

That matters when more than one thing writes the same field. If you read the list, add your item and send the whole array back, anything added between your read and your write is gone — and the response still says the update succeeded, because from the server’s side you asked for exactly that list. Sending add_to instead means two callers attaching a document at the same moment each keep theirs.

{
  "records": [
    { "id": "rec_...", "data": {}, "add_to": { "fld_attachments": ["fil_..."] } }
  ]
}

Use the plain data form when you genuinely mean “the list is now this” — reordering it, or clearing it. A field may appear in data or in add_to/remove_from, not both.

Record queries

Record queries are executed server-side with the same filter engine used by the Lotics interface. Complex filters (nested AND/OR conditions, linked record lookups, date comparisons) perform identically through the API and in the app.


Pagination

All list endpoints use cursor-based pagination for consistent results even during concurrent writes.

Parameter Default Maximum Description
limit 100 1,000 Number of items per page
cursor (none) – Cursor from a previous response’s next_cursor field

How it works

  1. Make your initial request without a cursor parameter.
  2. If more results exist, the response includes a next_cursor field.
  3. Pass the next_cursor value as the cursor query parameter in your next request.
  4. Repeat until next_cursor is absent, meaning you have reached the last page.

Rate limits

Limits are counted per 60-second window, and separately per client IP and per member. The budget depends on what the request does, not on which endpoint it hits:

What the request does Per IP Per member
Read (GET, record queries, aggregates) 1,200 / min 1,200 / min
Write (create, update, delete, presign) 600 / min 600 / min
Download 300 / min 600 / min
Upload bytes through the API 200 / min 400 / min
Sign in, password reset, token exchange 20 / min 30 / min

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. When you exceed a limit you receive 429 Too Many Requests with Retry-After set to the seconds until the window resets — read that header rather than guessing, and back off exponentially on repeated 429s. Contact us if your use case needs a higher ceiling.


Error responses

Every non-2xx response — including an unmatched path — is JSON with the same three fields:

{
  "code": "not_found",
  "message": "Resource with key=rec_9tK2 not found",
  "hint": "The record may have been deleted. List the table's records to confirm."
}
  • code is stable and is the only field to branch on.
  • message is human-readable prose. It gets reworded; do not match on it.
  • hint is present only when there is an actionable next step, and is absent otherwise.

Some errors add their own fields alongside these — a 409 from a version conflict carries current_version_id, and a rejected record write or a refused app payload carries field_errors.

Status Code Meaning
400 bad_request Invalid request body, missing required fields, or malformed parameters
400 hook_error A table check refused the write. See field_errors.
401 unauthorized Missing, invalid, disabled or expired API key
403 forbidden Authenticated, but not permitted — or the organization has been deleted
404 not_found Resource does not exist, is not visible to this caller, or the path matches no route
409 conflict Resource conflict (e.g. duplicate name, stale version)
429 rate_limit Rate limit exceeded. Read Retry-After.
500 internal_error Unexpected server error
503 service_unavailable An upstream dependency is unreachable. Retry later.

The same shapes are published in the OpenAPI document as the Error schema, referenced by every operation.

Every response carries an x-request-id header — your own if you sent one (letters, digits, ., _, : or -, up to 128 characters), otherwise one we generate. Log it beside a failure and quote it to us: it is the token that finds that exact request. A 401 also carries WWW-Authenticate: Bearer, with error="invalid_token" added when the key you sent is what was refused, which is what separates “authenticate” from “stop retrying the key you hold”.


Calling an app from your own site or server

An app declares its capabilities up front — named queries that read, workflows that write, and agents that carry a task through (see Apps). Those declarations are addressable endpoints, so your own site can use Lotics as the system of record.

Call them from your server, with an API key. Create the key at Settings → API keys with Only selected ones, and give it just the app your site uses: the key then reaches that app and nothing else. Your visitors’ browsers talk to your server, and your server talks to Lotics — a key never goes into a page.

An app without screens (queries and workflows only) is always private: this is how it is called. Sharing an app publicly is for its own screens, which anyone with the link can open.

The three endpoints

POST /v1/apps/{app_id}/queries/{alias}              # run a declared query
POST /v1/apps/{app_id}/workflows/{alias}/execute    # run a declared workflow
POST /v1/apps/{app_id}/agents/{alias}/runs          # start a declared agent

A query call fills the template’s declared parameters and may narrow further within what the query already returns:

const res = await fetch(
  "https://api.lotics.ai/v1/apps/app_7Qm2xR/queries/open_orders",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.LOTICS_API_KEY}`,
    },
    body: JSON.stringify({ params: { branch: "north" }, limit: 50 }),
  },
);
const { rows } = await res.json();

The reply carries rows (one object per row, keyed by the column names the query declares), columns describing them, and — when you asked for them — total, next_cursor and truncated.

A workflow call sends the typed inputs the alias declares:

const res = await fetch(
  "https://api.lotics.ai/v1/apps/app_7Qm2xR/workflows/submit_enquiry/execute",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.LOTICS_API_KEY}`,
    },
    body: JSON.stringify({ inputs: { company: "Acme", email: "buyer@acme.example" } }),
  },
);
const result = await res.json();

The reply is status (success or error), an optional message, data when the workflow returned any, files for anything it generated, and field_errors keyed by input name when it refused a value — which is what a form renders against the control that was wrong.

A caller in the app’s own organization also gets side_effects, the run’s own record of what it changed: the records it created grouped by table, the files it produced, and the steps that cannot be undone. Beside it come counts of the records the run created, updated, deleted, restored, and locked or unlocked, and refused: true on an error the workflow refused rather than failed on — the refused write never landed, though what an earlier step wrote did, and the counts say so. Any other caller, a visitor with no key included, gets the workflow’s answer alone, because those ids name rows in a workspace such a caller cannot read, delete or audit.

When the run itself fails, rather than the workflow refusing, a caller from outside the app’s organization reads one generic message in the language its Accept-Language names, beside execution_id — the run’s id, under which the app’s owner reads what went wrong.

Every call carries Authorization: Bearer ltk_…. The one caller without a key is a visitor on a public app’s own screens.

Sending files

A workflow or agent input of type file takes a file id, never the bytes. Turn each file into an id with two calls to the same app, and PUT the bytes straight to storage between them. Do this from your server, with your key: storage accepts a PUT from a browser only on the app’s own pages, so a page on your site sends the file to your server and your server uploads it.

// On your server. `photo` is { name, type, bytes } from your own upload route.
const app = "https://api.lotics.ai/v1/apps/app_7Qm2xR";
const post = (path, body) =>
  fetch(`${app}/${path}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.LOTICS_API_KEY}`,
    },
    body: JSON.stringify(body),
  }).then((res) => res.json());

const { file_id, file_storage_key, upload_url } = await post("files/upload-url", {
  filename: photo.name,
  mime_type: photo.type,
  file_size: photo.bytes.byteLength,
});
await fetch(upload_url, { method: "PUT", headers: { "Content-Type": photo.type }, body: photo.bytes });
await post("files/complete", { file_id, file_storage_key, filename: photo.name });

await post("workflows/request_quote/execute", { inputs: { photos: [file_id] } });

upload_url is signed for the exact type and size you declared and expires after 10 minutes. files/complete checks the stored object against that size, and only then is file_id accepted by the app’s inputs. One file is at most 25 MB, of any type. Send your key on both calls; a caller with no key may upload 500 MB per hour from one address, and every visitor behind your server shares that one address.

The file is stored in the app’s workspace whoever uploads it, and an app’s file inputs accept only files from that workspace. Every app whose published API takes a file also lists both calls in its OpenAPI document.

Running an agent

An agent call starts a run and hands back the run as it happens. It sends a session_id of your own choosing — any non-empty string — plus the input the alias declares:

const res = await fetch(
  "https://api.lotics.ai/v1/apps/app_7Qm2xR/agents/triage_enquiry/runs",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.LOTICS_API_KEY}`,
    },
    body: JSON.stringify({ session_id: "web-42", input: { enquiry_id: "rec_9tK2" } }),
  },
);
const runId = res.headers.get("x-app-agent-run-id");
const runToken = res.headers.get("x-app-agent-run-token");

A run sent with a key reads the earlier runs carrying that same session_id as context, which is how a follow-up builds on what came before. A run sent without one reads none — the thread a visitor could ask for is a thread they could also guess their way into — so on a public app the id groups your own runs and nothing more; send everything such a run needs in its input.

The reply is a stream of server-sent events carrying what the agent says and does while it works. The result is not in it — read that at the run:

GET /v1/apps/{app_id}/agent-runs/{run_id}

run.status is running, awaiting_input, completed, error or aborted, and run.output is null until the run settles. On completed it is the object the alias declares, or the text the agent finished with when the alias declares no outputs. awaiting_input means the agent asked the caller a question: the same read carries it as pending_interactive, the answer goes to POST /v1/apps/{app_id}/agent-runs/{run_id}/continue, and POST /v1/apps/{app_id}/agent-runs/{run_id}/cancel ends the run instead.

The run finishes on our side whether or not you are still listening, so a dropped stream loses nothing — poll the run instead. A call with no key receives x-app-agent-run-token beside the run id; send it back as that same header on the read, and it reaches that one run and nothing else.

An agent run spends your organization’s credits, and the caller is the one who decides when. On a publicly shared app that caller is a stranger. What bounds them is the per-address window in the table below, a ceiling on how many runs one app may have in flight for outsiders at once, and your organization’s own credit limit — so give a public app an agent only when strangers running it is what you meant.

What to expect when something goes wrong

Status Meaning What to do
200 with status: "error" The workflow refused the request, or the run failed Show message, and field_errors against their controls. A failed run also carries execution_id — give it to the app’s owner, who reads the error under it
400 The body did not match what the alias declares, or the alias does not exist Read field_errors — one sentence per input or param it refused, keyed by name, so a form marks the control; message says the same in one line
401 / 403 The app is not shared publicly and the request carried no usable credential Share the app, or send a key
402 An agent run was refused because the organization that owns the app has spent its credits That is the app owner’s to settle — tell them; retrying does not change the answer
409 An agent run could not start: this key already has its ceiling of runs in flight, or the app has its ceiling of runs from callers with no key Wait for one to settle, then send it again
413 A caller with no account sent a request body over 256 KB Send less. File bytes never travel in the body — see Sending files
415 The body was not sent as Content-Type: application/json Send it with Content-Type: application/json
429 A caller with no account exceeded 60 workflow runs or 60 agent runs per minute — two separate allowances, each counted per app and per address — or uploaded more than 500 MB in an hour from one address Read Retry-After — it carries the seconds until the window resets — and back off

Why the calls come from your server

The API answers a browser only on Lotics’ own pages, so JavaScript on your site cannot call it directly — and should not: anything a page ships is readable by anyone who opens it, and a key in a page is a key anyone can copy. Your server holds the key, checks what your form sent, and calls the app. Every framework a site is built on has server routes for this (Next.js API routes, Netlify and Vercel functions, an Express handler).

A site with no server of its own can link to, or embed, an app’s own screens instead: shared publicly, they work for anyone with the link.


Publishing an app’s API

An app’s declarations change as the app is improved. Publishing its API turns those declarations into a contract — a numbered snapshot of exactly what each alias accepts and returns — so that your own code, which nobody here can redeploy, is not broken by a change made in the workspace.

lotics run publish_app_api '{"app_id":"app_..."}'     # snapshot the contract; prints the version and any warnings
lotics run unpublish_app_api '{"app_id":"app_..."}'   # end the promise

The snapshot’s OpenAPI 3.1 document is served directly, for a generator to fetch:

GET https://api.lotics.ai/v1/apps/{app_id}/openapi.json

It describes one operation per query, per workflow and per agent, with the real app id in the path, so openapi-generator turns each alias into its own named, typed method — plus the one read where every agent run’s result is collected, whichever alias produced it. It renders the published snapshot, not the app’s current state — a client generated from it matches what the app promised.

What counts as a breaking change

From the publish onward, a change to the app is a release. Anything that only ADDS is applied and re-snapshotted at the next version with nothing to do. A change that would break a caller is refused, and the refusal names each one:

  • a query, workflow or agent that is no longer there, or a column or output that disappeared;
  • a column or output that changed type, or that may now be empty where it never was;
  • a required input added, an input removed, or an input’s accepted values narrowed;
  • an input shape declared on an agent that had none, because from then on a caller’s own keys are refused;
  • an agent’s result changing kind, either way — an object where it was free text, or free text where it was an object.

The reverse direction is always safe, which is worth knowing because it is not symmetrical: a new optional input, a new column, an output value that can no longer appear, an agent’s input shape dropped so that any object is accepted again — none of those break anyone reading or calling what they already were.

The way through a refusal is a new alias. Declare the new shape beside the old one, move your callers over, and retire the old alias once nothing calls it — your consumers switch when they are ready instead of when you deploy. When you genuinely mean to break them — you own both ends, or nothing is calling it yet — pass "acknowledge_breaking_api_change": true on the write (set_app_queries, set_app_workflow, set_app_agent, remove_app_binding, apply_model, rollback_app), which carries it out and snapshots the new contract. It counts only from an organization admin.

A change to the underlying table is refused the same way, and has no acknowledgment: a promise is changed from the app side, where a new version is taken.

Scripting against this rather than reading the terminal? Both refusals are 409, with their own codes and a structured list of what would break — breaking_api_change for a change to the app, and published_api_contract for a change to a table, which carries the app_id of each app it would break. Branch on code and read breaking_changes, which sits beside code and message at the top level of the body; the message lists at most five changes per app, the field carries all of them. The CLI prints the server’s sentence, not the list — read the list from the response.

Publishing refuses one thing outright: a query that does not name the columns it returns. Those names would come from the table and would change under your callers whenever the table did, so they are not the app’s to promise. The refusal names each query and shows the fix.


Webhooks

Webhooks run inbound: your system calls Lotics, and the call starts an automation. Create an automation with a Receive webhook trigger and Lotics issues a URL with a random 64-character path:

https://api.lotics.ai/v1/webhooks/triggers/{webhook_path}

POST a JSON body to it and the automation runs, with the body available to every step — so a webhook can create records, update statuses, generate a document, or send a notification, without any of that logic living in your caller.

Securing the endpoint

The path is unguessable, and you can require a signature on top of it. Set a shared secret on the trigger, then send X-Webhook-Signature as the hex HMAC-SHA256 of the exact request body:

X-Webhook-Signature: hmac_sha256_hex(secret, raw_request_body)

Sign the raw bytes you send, not a re-serialized copy — a body that is parsed and re-encoded produces a different signature. A request with a wrong or missing signature is rejected and no automation runs.

One webhook path accepts 600 deliveries per minute and a body of up to 5 MB. The delivery rate is counted for the path itself rather than per sender, because a form platform or an ERP relay presents its own address for everyone it forwards. Past either, the reply is 429 with Retry-After — a sender that retries on it loses nothing.

Sending form submissions from a form platform

Typeform, Jotform, Google Forms via a connector, a website builder’s form block — anything that can POST to a URL can feed an automation. Three things decide whether it works, and each fails quietly:

  • Send JSON. A body that is not valid JSON arrives as raw text, so the form-urlencoded default most platforms ship with hands the automation one string instead of fields. Switch the payload to JSON in the platform’s webhook settings.
  • Read the body defensively. The body is whatever the sender posted, so the automation checks its shape before reading a field — if (isObject(trigger.body)), then toString(trigger.body.email) per field. Reaching straight into trigger.body.email is refused when the automation is saved.
  • A failing automation answers the sender an error. The HTTP response is the automation’s outcome, so a step that cannot complete becomes a delivery failure in the form platform — which is right for a system that retries, and wrong for anything a person is waiting on. For a form a person fills in on your own website, put an app in front of it instead (see Calling an app from your own site or server): a declared workflow validates typed inputs and tells the submitter which field was wrong.

Reacting to changes in Lotics

There is no outbound event subscription: Lotics does not POST to a URL of yours when a record changes. Do that with an automation instead — a table workflow on after_create / after_update with an HTTP request step calls your endpoint, and unlike a fixed event catalogue you decide there exactly which records qualify and what the payload contains.


MCP Server

Lotics provides a Model Context Protocol (MCP) server that exposes the same capabilities as the REST API through the MCP standard. This allows AI assistants and LLM-based tools to interact with your Lotics data directly.

See the dedicated MCP Server documentation for setup instructions and available tools.


CLI and SDK

Lotics provides a command-line interface and Node.js SDK for scripting, automation, and integration.

Installation

curl -fsSL https://lotics.ai/install.sh | bash

On Windows, in PowerShell: irm https://lotics.ai/install.ps1 | iex. It downloads one compiled executable — no Node.js and no package manager.

From the command line

The CLI is the fastest way for an AI coding agent to work against a workspace — it authenticates once and exposes the same capabilities as the API:

lotics auth signup you@company.com
lotics run query_tables '{}'
lotics run create_records '{"table_id":"tbl_...","records":[{"fld_...":["opt_..."]}]}'

lotics tools lists every tool the run verb can reach, and lotics tools <name> prints one tool’s input schema. lotics docs cli_reference prints the per-command reference.

Generating a typed client

For a program rather than an agent, generate a client from the OpenAPI document — every operation carries a unique operationId, typed parameters and a response schema, so the generated methods are named and typed rather than stringly-addressed:

npx @openapitools/openapi-generator-cli generate \
  -i https://lotics.ai/openapi.json \
  -g typescript-fetch \
  -o ./lotics-client

See CLI reference for the full command-line documentation.


OpenAPI specification

The OpenAPI 3.1.0 specification is published at:

https://lotics.ai/openapi.json
https://api.lotics.ai/v1/openapi.json

Both serve the same document; the first is an alias, for tools that probe the main domain. It is generated from the route schemas on every request, so it cannot drift from the API: every operation carries a unique operationId, a description, typed parameters, a response schema and the shared Error schema for its failure cases.

Import it into Postman, Insomnia, any OpenAPI-compatible code generator, or an agent framework that builds function-calling tools from a spec.

Use openapi-generator to generate typed SDKs in TypeScript, Python, Go, Java, and other languages:

npx @openapitools/openapi-generator-cli generate \
  -i https://api.lotics.ai/v1/openapi.json \
  -g typescript-fetch \
  -o ./lotics-client

Common use cases

  • System sync: Keep Lotics in sync with external systems (ERP, CRM, e-commerce) by pushing and pulling records through the API.
  • Custom dashboards: Build dashboards that pull live data from Lotics tables using query and aggregate endpoints.
  • Automated record creation: Create records from external events – form submissions, payment confirmations, shipping updates.
  • Report generation: Query and aggregate record data programmatically to generate reports.
  • Document automation: Fill document templates with record data to produce PDFs and Excel files on demand.
  • CI/CD integration: Use API keys in pipelines to create records, update statuses, or trigger workflows as part of your deployment process.

Frequently asked questions

Is there a rate limit on the API?

Yes — counted per 60-second window and per route class, not per second. Reads get 1,200 per minute, writes 600, uploads 200. Every response carries X-RateLimit-Remaining; a 429 carries Retry-After with the seconds until the window resets. See Rate limits for the full table.

Can I build my own frontend on Lotics?

Yes. An app’s declared queries, workflows and agents are addressable endpoints, so your own site or your own server can read, write and run work through them while Lotics stays the system of record. Publish the app’s API and you get a numbered contract plus an OpenAPI document to generate a client from, and a change made in the workspace can no longer break your code without someone saying so. See Calling an app from your own site or server.

Can I use the API to create workflows programmatically?

Yes. The workflows endpoint supports full CRUD. You can create triggers, define steps (including conditionals, loops, and AI actions), and deploy workflows entirely through the API. Workflow execution history is also available via the API.

How do I handle file uploads through the API?

Use the Files upload endpoint with a multipart/form-data request. The response returns a file ID that you can assign to a file field when creating or updating records. File download URLs are signed and time-limited for security.

Can I test the API without affecting production data?

Create a separate organization for development and testing. API keys are organization-scoped, so your test key will only access test data. There is no additional cost for development organizations.

Is the OpenAPI spec available for code generation?

Yes. The OpenAPI 3.1.0 spec at https://api.lotics.ai/v1/openapi.json can be imported into tools like openapi-generator, Postman, or any OpenAPI-compatible client to generate typed SDKs in TypeScript, Python, Go, Java, and other languages.

How does cursor pagination differ from offset pagination?

Cursor pagination uses an opaque token (next_cursor) instead of page numbers. This ensures consistent results even when records are created or deleted between requests. With offset pagination, insertions and deletions can cause you to skip records or see duplicates. Cursor pagination avoids these problems entirely.

How do I get notified when a record changes?

Build an automation. A table workflow on after_create or after_update can call your endpoint with an HTTP request step, and you decide in the workflow which records qualify and what the payload looks like. Lotics has no outbound event subscription to register — its webhooks point the other way, from your system into an automation.

Can AI assistants interact with the API?

Yes. The MCP Server exposes the same capabilities as the REST API through the Model Context Protocol standard. AI assistants and LLM-based tools can query data, create records, trigger workflows, and generate documents. See the MCP Server documentation for setup.

How do I filter records by multiple conditions?

The query endpoint supports nested AND/OR filter groups. Each filter specifies a field, operator, and value. You can combine filters into groups with and/or logic. The filter engine is the same one used in the Lotics interface, so any filter you build in the UI can be replicated through the API.

What field types are supported?

All field types available in the Lotics interface are supported through the API: text, number, date, select, multi-select, checkbox, linked records, files, formula, rollup, and lookup. Computed fields (formula, rollup, lookup) are read-only – their values are calculated automatically based on their configuration.

Book a demo

30 minutes, scheduled by phone or Zalo.