Data API Concepts¶
The two Data APIs solve two different problems. Read this page once and the rest of the section will make sense: the references, the quickstart, and the use cases.
Pull vs push¶
The REST API is request/response. You send an HTTP request, it runs against a Center, and you get one result back. It is the right tool when you want an answer now: run a query, insert rows, or call a procedure and read what it returned. Nothing keeps running after the response is sent.
The WebSocket streaming API is subscribe-and-stream. You open a long-lived connection, subscribe to a Center table with a SELECT, and the Center sends you the current contents once (a snapshot) and then a continuous stream of row changes (updates) until you unsubscribe or disconnect. It is the right tool when you want to keep watching something that changes.
A useful way to hold it: REST answers a question; WebSocket watches a table. The two compose: you write with REST and watch with WebSocket. The Quickstart does exactly that.
| REST API | WebSocket streaming API | |
|---|---|---|
| Model | Request/response (pull) | Subscribe, then live push |
| Lifetime | One request, one response | Long-lived connection |
| Can write data | Yes: INSERT, UPDATE, DELETE, CALL |
No: read/subscribe only |
| Multiple tables / joins | Yes (full Center SQL) | No: one table per subscription, no joins |
| Delivery | The whole result at once | Snapshot, then per-row add / update / delete |
Both surfaces are off until you enable them¶
This is the single most common reason "nothing works." A fresh AmiCore process does not expose either surface:
- The REST query endpoint is excluded by the shipped setting
ami.rest.plugin.classes=*,!QUERY,!ADMIN. The REST server is running (the informational endpoints answer), but/3forge_rest/queryreturns nothing useful until you addQUERY. - The WebSocket server opens a port only when
ami.websocket.portis set. The shipped configuration leaves it commented out, so the server never starts and the log saysWebSocket server disabled (no ami.websocket.port configured).
Turning both on is a two-line change: see Configuration.
Creating a streamable table¶
Both APIs work against Center tables, so the first step in most integrations is creating one, and there is one rule that trips up newcomers: the table must be PUBLIC.
CREATE PUBLIC TABLEis shared, live state that REST and WebSocket clients can see, query, and subscribe to. A plainCREATE TABLEreturns"status":"ok"but is transient: a later stateless REST call or a WebSocket watcher getsUnknown table. Always usePUBLICfor these APIs.USEoptions are only valid on aPUBLICtable.Broadcastcontrols whether a table streams over the WebSocket. It is on by default, so aPUBLICtable streams without it;Broadcast="false"turns streaming off.RefreshPeriodMssets its refresh cadence.PersistEnginemakes a table's rows survive a restart: without it the definition survives but the data does not (see the restart warning below). Options are space-separated, not comma-separated.- Primary keys are a separate
CREATE INDEX, never a clause insideCREATE TABLE.CREATE INDEX <name> ON <table>(<col> HASH) USE Constraint="PRIMARY"sets the key.Constraint="PRIMARY"also gives upsert semantics: re-inserting a row with an existing key overwrites it (last-write-wins, only the supplied columns are written) rather than adding a duplicate.
CREATE REALTIME TABLE and a PRIMARYKEY clause inside CREATE TABLE are not valid AMI syntax; both fail to parse.
Common column types are String, Long, Integer, Double, Float, Boolean, Date, UTC, and UTCN.
Date columns are easy to corrupt
Date holds a calendar date only. For an instant, use a UTC (millisecond) or UTCN (nanosecond) timestamp column, or store a Long of epoch milliseconds and convert in your client. Timestamp, DateTime, and Time are not type names and fail with Invalid type. Write a Date as a quoted "YYYY-MM-DD" string; it reads back as "YYYY-MM-DDT" (a trailing T, with no time component and no time-zone indicator, so it is not valid ISO 8601, and the zone is not carried on the wire; agree it out of band). There is no now() function; timestamp() returns the current time in epoch milliseconds.
A bare integer such as 20260914 is accepted without error and silently stored wrong (year 20260). Always quote the date.
Write a UTC value as epoch milliseconds, and a UTCN value as epoch nanoseconds. Both also accept an ISO 8601 string with a T and a Z, such as "2026-09-17T10:30:00Z", with fractional seconds kept to the column's precision ("2026-09-17T10:30:00.123456789Z" for UTCN). A string without the T, such as "2026-09-17 10:30:00", is rejected with Invalid value for column.
Rows are lost on restart unless the table persists
A CREATE PUBLIC TABLE keeps its definition across a process restart, but its rows do not: after a restart the table is present and empty. Only a table created with USE PersistEngine="…" retains its data. A quickstart table (no PersistEngine) silently empties on every restart; reload its rows, or add a persist engine if the data must survive.
Authentication¶
The 2 surfaces authenticate separately, each with its own setting:
- REST: HTTP Basic in the
Authorizationheader (curl -u user:pass), or the Web-session cookie of a browser already logged in to the Web. Credentials are checked byami.rest.auth.plugin.class, which defaults toami.auth.plugin.class, so REST accepts the same users as Web logins. - WebSocket: HTTP Basic on the connection handshake only; the Web-session cookie is not accepted. Credentials are checked by
ami.websocket.auth.plugin.class, which ships asDISABLEDand accepts every connection without a credential check, asanonymouswhen it sends no credentials. Because browsers cannot set theAuthorizationheader on the handshake, a browser embeds the credentials in the URL (wss://user:pass@host:3410/center). The browser sends them only after the server rejects the first handshake with401, which happens only when an authentication plugin is configured.
There are no built-in API keys or bearer tokens and no CORS handling in the REST framework itself: access is controlled by authentication, TLS, and optional IP allow-lists. For production, secure the WebSocket with wss:// or a reverse proxy rather than exposing plain ws://. See Configuration.
Configuring an identity provider¶
ami.auth.plugin.class is a general extension point, not specific to these two surfaces: it authenticates Web logins, and REST by default. To check WebSocket credentials against the same source, set ami.websocket.auth.plugin.class to the same plugin. LDAP and SAML are not bundled by default; they're separately-distributed plugins (amiplugins/amildap, amiplugins/amisaml) you request from 3forge, confirmed live: this instance's plugin registry carries only ACCESS_FILE, DISABLED, and 3FWEBSITE. For configuring those plugins, see Authentication.
Whatever plugin you configure, the wire format each surface expects here is unaffected: see REST API → Authentication.
WebSocket filters use AMI expression syntax, not ANSI SQL¶
A WebSocket subscription is written as a SELECT, but its WHERE clause is an AMI expression, evaluated by the Center, not ANSI SQL. Two differences bite newcomers:
- Equality is
==, not=. - String literals use double quotes:
SYM == "IBM". And because the whole statement travels inside a JSON string, those double quotes are JSON-escaped:
An ANSI-style WHERE SYM = 'IBM' is the case to watch for, because it fails quietly and in the unsafe direction. A single = is not an equality operator here, so the clause is not a boolean; a WHERE the Center cannot evaluate is not reported. The subscription delivers every existing row unfiltered in its initial load, then receives no further changes: no updates, no new rows, and no deletes. You get no error frame and no empty result to tip you off, just an unfiltered view that looks filtered and then stops moving. Test a filtered subscription against a row it should exclude.
The REST /query endpoint evaluates WHERE with the same expression syntax, so == is required there too. It is stricter about it: a non-boolean WHERE is rejected outright with WHERE clause must evaulate to boolean, not: String. Quoting is the one thing that differs, since REST accepts either single or double quotes around a string literal.
What the WebSocket API does not do (in this version)¶
- No writes. You cannot insert or update over the WebSocket: send those over REST.
- One table per subscription, no joins. Subscribe to each table separately; you cannot subscribe to the same table twice on one connection with different filters.
- No request/response query. There is no "run this SQL once and return the rows" call over the WebSocket: that is what REST
/queryis for.
A note on example ports¶
Throughout this section the examples use 33332/33333 for the Web HTTP/HTTPS ports (which the REST API rides) and 3410 for the WebSocket port. The Web ports are the shipped defaults. The WebSocket port has no default: the server stays off until ami.websocket.port is set, and 3410 is the value in the shipped, commented-out example line. Substitute your deployment's actual ports.