Skip to content
Connect > Data APIs

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/query returns nothing useful until you add QUERY.
  • The WebSocket server opens a port only when ami.websocket.port is set. The shipped configuration leaves it commented out, so the server never starts and the log says WebSocket 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 TABLE is shared, live state that REST and WebSocket clients can see, query, and subscribe to. A plain CREATE TABLE returns "status":"ok" but is transient: a later stateless REST call or a WebSocket watcher gets Unknown table. Always use PUBLIC for these APIs.
  • USE options are only valid on a PUBLIC table. Broadcast controls whether a table streams over the WebSocket. It is on by default, so a PUBLIC table streams without it; Broadcast="false" turns streaming off. RefreshPeriodMs sets its refresh cadence. PersistEngine makes 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 inside CREATE 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.

1
2
3
CREATE PUBLIC TABLE QUOTES (SYM String, bid Double, ask Double, last Double, volume Long)
    USE Broadcast="true" RefreshPeriodMs="100";
CREATE INDEX QUOTES_PK ON QUOTES(SYM HASH) USE Constraint="PRIMARY";

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 Authorization header (curl -u user:pass), or the Web-session cookie of a browser already logged in to the Web. Credentials are checked by ami.rest.auth.plugin.class, which defaults to ami.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 as DISABLED and accepts every connection without a credential check, as anonymous when it sends no credentials. Because browsers cannot set the Authorization header 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 with 401, 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:
{"action":"subscribe","sql":"SELECT bid,ask FROM QUOTES WHERE SYM == \"IBM\"","oof":false}

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 /query is 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.