Skip to content
Connect > Data APIs

REST API

The 3forge REST server exposes a small set of endpoints under the path prefix /3forge_rest. It rides the Web component's HTTP/HTTPS port by default, so the base URL looks like https://<host>:33333/3forge_rest/… (or http://<host>:33332/3forge_rest/…). Every default endpoint except /3forge_rest/mcp is a GET request; the MCP endpoint takes a POST with a JSON-RPC body.

Enable the query endpoint first

The SQL endpoint /3forge_rest/query is not loaded by default (ami.rest.plugin.classes=*,!QUERY,!ADMIN). The informational endpoints below work out of the box, but you must add QUERY to run SQL. See Configuration.

Endpoints

Endpoint Purpose Auth Loaded by default
/3forge_rest/query Run a Center command: SELECT, INSERT/UPDATE/DELETE, CALL a procedure, SHOW … Yes No: add QUERY
/3forge_rest/version Build and version details Yes (admin) Yes
/3forge_rest/whoami The authenticated username and its properties Yes Yes
/3forge_rest/stats JVM and per-component runtime statistics No Yes
/3forge_rest/whatsmyip The caller's IP and request headers No Yes
/3forge_rest/admin Console/admin command execution Yes + EXECUTE permission No
/3forge_rest/mcp JSON-RPC endpoint for AI/agent tooling Yes Yes
/3forge_rest Lists the available endpoints No Yes

/3forge_rest/mcp is a separate JSON-RPC endpoint for AI/agent tooling built on the same REST server; a query issued over it behaves differently from /3forge_rest/query, see Operating over MCP.

GET /3forge_rest/query

Runs one Center command supplied in the cmd parameter and returns the result.

Parameters

Parameter Default Meaning
cmd (required) The Center command: a SELECT, an INSERT/UPDATE/DELETE, a CALL <proc>, SHOW TABLES, etc.
db center Which Center to run against. An unknown name returns an error listing the known Centers.
ds AMI The datasource to run against.
timeout 60000 Timeout in milliseconds.
limit 10000 Maximum rows returned.
display text Output format: text, pipe, json, jsonRows, or jsonMaps.
show_plan off on includes the query plan / tracked events in the response.
string_template off on interprets ${…} templating in cmd instead of treating it literally. See the warning below.

string_template=on makes cmd a template, not a literal

With string_template=on the server interpolates ${…} expressions inside the statement before running it. That is useful for trusted server-side templating, and it means a statement built from caller-supplied input becomes an injection vector: an attacker who controls any part of cmd can inject an expression that is evaluated server-side.

Leave it off, which is the default. Where it is needed, build cmd only from values your own code controls, and never interpolate request parameters, form fields, or anything else originating with the caller.

Response formats

The display parameter selects the shape:

  • text (default): a human-readable ASCII table. Good for eyeballing in a terminal, not for parsing.
  • pipe: pipe-delimited. Each table starts with a header line title|rowCount|type|name|type|name… (interleaved column types and names, not a plain name header), then one line per row. Split on |.
  • json: tables is an array of table objects, column-oriented: {title, columns:[{name, type, values:[…]}]}.
  • jsonRows: tables is an array of table objects: {title, columns:[{name, type}], rows:[[…], …]}.
  • jsonMaps: tables is an object keyed by table name, each value a list of row objects. Easiest to consume from a program, but the keys within each row are alphabetized, not in SELECT order, so use json/jsonRows if you need the declared column order.

tables is an object for jsonMaps, an array for json/jsonRows

With jsonMaps you reach the rows by table name: body["tables"]["QUOTES"]. With json and jsonRows, tables is an array of table objects: body["tables"][0]. Code that switches display must switch how it reaches the rows, or it fails with a type error.

Every JSON response also carries top-level fields:

Field When Meaning
status always "ok" or "error"
durationNanos always Server execution time in nanoseconds
rowsAffected always Rows actually added or changed, not rows matched. 0 for a SELECT or DDL; 1 for an INSERT of a new key; 0 for an upsert that overwrites an existing key (even though the row changed); for UPDATE/DELETE, the number of rows whose values changed. A no-op UPDATE and one matching nothing both report 0. See Insert rows and call a procedure.
returnValue always Present on every response; null unless a CALL returned a value. A scalar SELECT does not set it: SELECT 1+1 AS two returns returnValue:null and delivers the result under tables with the key _. A procedure with no explicit return statement still populates it, with the value of the script's last evaluated statement: see Insert rows and call a procedure
returnType with a non-null returnValue Always the literal string Object, whatever the value's actual type. A procedure returning a double, a string, and an int all report Object, so do not branch on it. The value itself is correctly typed in the JSON
autogenKeys when present Auto-generated keys from an insert
error on a statement failure {message[, atLine, atChar]}: atLine/atChar are 0-based and locate an expression parse error
plan with show_plan=on Tracked execution events
tables when the command returns rows The result set(s), shaped per display (object for jsonMaps, array for json/jsonRows)

Four kinds of error response

Errors come back in 4 different shapes, and a client must handle all of them. Authentication failures (401 and 403) are covered under Authentication.

  • Statement errors: the command reached the Center but failed (unknown column, type mismatch, non-boolean WHERE). HTTP 200, JSON body, status:"error", and an error object {message, atLine, atChar} (atLine/atChar 0-based).
  • Request errors: the request was rejected before running. HTTP 400, body is plain text or HTML with no status field at all: e.g. cmd required, unknown center: …, or <i>display</i> must be either: ….
  • Server errors: the request timed out on the server or failed unexpectedly. HTTP 500, plain-text body: REQUEST TIMEOUT ERROR or UNKNOWN ERROR FROM AMI REST PLUGIN, followed by a stack trace unless ami.rest.show.errors=false.
  • Unknown endpoint: the path names no loaded endpoint, because of a typo or because the endpoint is not enabled in ami.rest.plugin.classes. HTTP 404, HTML body: Endpoint not found: /<name> followed by links to the loaded endpoints. It is returned before credentials are checked.

Check the HTTP status first, then the JSON status. Calling resp.json() on a 400, 404, or 500 response throws, because the body is not JSON. Note that display is case-sensitive: jsonmaps is a 400, jsonMaps is correct.

For date and time columns and their wire format, see Concepts → Creating a streamable table.

Example: query as JSON

curl -u username:password -X GET \
  "https://localhost:33333/3forge_rest/query?cmd=SHOW+TABLES&display=jsonRows"

With SQL that contains spaces or special characters, let curl URL-encode it:

1
2
3
curl -sk -u username:password -G \
  --data-urlencode "cmd=SELECT * FROM QUOTES" \
  "https://localhost:33333/3forge_rest/query?display=jsonMaps"

(-k accepts a self-signed development certificate; drop it in production.)

On Windows PowerShell, pass Basic auth as a header:

1
2
3
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("username:password"))
$headers = @{ Authorization = "Basic $auth" }
Invoke-WebRequest -Uri "https://localhost:33333/3forge_rest/query?cmd=SHOW+TABLES&display=jsonRows" -Headers $headers

Authentication

/3forge_rest/query requires authentication. Two mechanisms are accepted:

  • HTTP Basic: Authorization: Basic base64(user:pass), i.e. curl -u user:pass. Validated against the deployment's configured authenticator.
  • Web-session cookie: a browser already signed in to the Web reaches the endpoint using its existing session, with no second login.

An unauthenticated request receives HTTP 401 with a WWW-Authenticate: Basic challenge; a disabled or locked user receives 403. There are no built-in API keys or bearer tokens. The wire format is always Authorization: Basic: configuring a different ami.auth.plugin.class (OAuth/SAML/OKTA/LDAP) changes how those Basic credentials are validated, never what the client sends; the REST framework has no code path for a Bearer token. The REST framework also performs no CORS handling.

Custom endpoints

Beyond the built-ins, a deployment can add its own endpoint under /3forge_rest/<name> by implementing a REST plugin and loading it via ami.rest.plugin.classes. This is the right path when you want to expose a narrow, purpose-built operation instead of general SQL. Implement the com.f1.ami.amicommon.rest.AmiRestPlugin interface, verified against current source at that exact package path.

Choosing between general SQL and a custom endpoint

Enabling QUERY and writing a custom endpoint solve different problems, and the choice depends on how much the caller is trusted rather than on which is newer or tidier.

What enabling QUERY actually grants. The endpoint requires authentication, runs each statement as the authenticated user with that user's Center permissions applied, and records the invoking username against the request. So its reach is exactly that identity's permissions, and sizing those permissions is the control that matters. It does not add an authorization layer of its own, and it accepts any statement the Center accepts, including writes and procedure calls.

Enable QUERY when the caller is inside your trust boundary:

  • A server-side integration you deploy and control, such as an ETL job or a scheduled reconciliation.
  • An internal analyst or reporting tool on a controlled network.
  • Ad-hoc work during development and evaluation.

Write a custom endpoint instead when any of these apply:

  • The caller is less trusted, or you do not control its code.
  • The operation is narrow enough to name, such as "submit an order" rather than "run SQL".
  • You need authorization per operation rather than per identity, or an audit record of intent rather than of statements.
  • The caller should not be able to reach tables beyond the one operation.

Restrict the endpoint's network exposure either way. Running arbitrary SQL over HTTP is reasonable inside a trusted network and rarely reasonable on an open one, which is why QUERY is excluded from the shipped default. Grant each integration its own identity with the narrowest permissions that let it work, rather than reusing an administrative account.

Related

Enabling the endpoint is covered in Configuration. The string_template parameter has its own warning above: it interpolates expressions server-side and must never receive caller-supplied input.

How a date comes back

A Date column renders differently per display, and neither form carries a time zone:

display A Date holding 2026-09-17 comes back as
text (default) 20260917, with no separators, no time, and no zone
json, jsonRows, jsonMaps "2026-09-17T", a string with a trailing T and no time or zone

A Date carries no time of day. For an instant, use a UTC (millisecond) or UTCN (nanosecond) timestamp column. These carry a zone in every format:

display UTC UTCN
text (default) 20260917 01:30:00.123 GMT for 13:30, a 12-hour time with no AM or PM The same as UTC, cut to milliseconds
json, jsonRows, jsonMaps "2026-09-17T10:30:00.123Z", ISO 8601 with 3 decimal places "2026-09-17T10:30:00.123456789Z", ISO 8601 with 9 decimal places

The text display does not say whether a time is before or after noon, so 01:30 can mean 01:30 or 13:30. Use a JSON display whenever the time of day matters, and for UTCN when the nanoseconds matter. Alternatively, store a Long of epoch milliseconds and convert in your client, which comes back as the plain number in every format. For the write-side rules, including the unquoted-integer trap, see Concepts.

What this does not support

  • No CORS headers. A browser page on another origin cannot call these endpoints directly. Put a proxy in front, or call from a server-side process.
  • No API keys and no OAuth. Authentication is Basic or cookie, delegated to ami.rest.auth.plugin.class, which defaults to ami.auth.plugin.class. There is no native token issuer.
  • No streaming. REST is request and response. For live row changes use the WebSocket streaming API.
  • No subscriptions over REST, and no one-off queries over the WebSocket. A WebSocket subscription is defined by a SELECT, but there is no call that runs a statement once and returns the result; that is what REST is for.
  • No offset parameter. To page through a large table, put LIMIT <offset>,<count> in the command, e.g: LIMIT 10000,10000. The limit parameter is only the default cap, applied when the statement has no LIMIT of its own.
  • A truncated result is not flagged. A result set is capped by limit, default 10000. When more rows match, the response returns exactly limit rows with status:"ok" and nothing indicating that rows were withheld: no count of matches, no truncation field, no warning. A client cannot tell a complete result from a capped one, so compare the row count you received against limit yourself, and treat equality as "possibly truncated".