HTTP/JSON API

Overview Endpoints

Action Method Endpoint

Get OpenAPI definition

GET

/api/v1/openapi.json

Get server status

GET, HEAD

/api/v1/ready

Check server liveness

GET, HEAD

/api/v1/health

Get server information

GET

/api/v1/server

Send server command

POST

/api/v1/server

List databases

GET

/api/v1/databases

Does database exist

GET

/api/v1/exists/{database}

Login (get auth token)

POST

/api/v1/login

Logout (invalidate token)

POST

/api/v1/logout

List active sessions

GET

/api/v1/sessions

Execute a query

GET

/api/v1/query/{database}/{language}/{query}

Execute a query

POST

/api/v1/query/{database}

Execute database command

POST

/api/v1/command/{database}

Begin a new transaction

POST

/api/v1/begin/{database}

Commit a transaction

POST

/api/v1/commit/{database}

Rollback a transaction

POST

/api/v1/rollback/{database}

Batch import vertices and edges

POST

/api/v1/batch/{database}

Overview Query & Command Parameters

Parameter Type Values

language

Required

sql, sqlscript, graphql, cypher, gremlin, mongo, and others

command

Required

encoded command string

awaitResponse

Optional

set synchronous (true, default) or asynchronous (false) command

limit

Optional

maximum number of results

params

Optional

map of parameters

serializer

Optional

graph, record, studio

autoCommit

Optional

Boolean. true to force atomic transaction, false to disable, omit for automatic behavior

Introduction

The ArcadeDB Server is accessible from the remote through the HTTP/JSON protocol. The protocol is very simple. For this reason, you don’t need a driver, because every modern programming language provides an easy way to execute HTTP requests and parse JSON.

For the examples in this chapter we’re going to use curl. Every request must be authenticated by passing user and password as HTTP basic authentication (in HTTP Headers). In the examples below we’re going to always use "root" user with password "arcadedb-password".

Under Windows (Powershell) single and double quotes inside a single or double quoted string need to be replaced with their Unicode entity representations \u0022 (double quote) and \u0027 (single quote). This is for example the case in the data argument (-d) of POST requests.

Token-Based Authentication

In addition to HTTP Basic Authentication, ArcadeDB supports token-based authentication. This allows you to login once with credentials and receive a token that can be used for subsequent requests, avoiding the need to send username and password with every request.

This is particularly useful when:

  • Building applications that make multiple API calls

  • You want to avoid storing credentials in your application

  • You need to manage user sessions with explicit logout capability

Authentication Flow:

  1. Call POST /api/v1/login with Basic Auth credentials to obtain a token

  2. Use the token in subsequent requests via Authorization: Bearer <token> header

  3. Call POST /api/v1/logout to invalidate the token when done

Example workflow:

# 1. Login and get a token
$ curl -X POST http://localhost:2480/api/v1/login \
       --user root:arcadedb-password

# Response: {"token":"AU-ArcadeDB_0-2af64e60-8455-423a-bc64-ed0e19729f04","user":"root"}

# 2. Use the token for subsequent requests
$ curl http://localhost:2480/api/v1/query/mydb/sql/select%201 \
       -H "Authorization: Bearer AU-ArcadeDB_0-2af64e60-8455-423a-bc64-ed0e19729f04"

# 3. Logout when done
$ curl -X POST http://localhost:2480/api/v1/logout \
       -H "Authorization: Bearer AU-ArcadeDB_0-2af64e60-8455-423a-bc64-ed0e19729f04"

Authentication tokens expire after a configurable period of inactivity (default is 30 minutes). See server.httpAuthSessionExpireTimeout to configure this timeout.

Token revocation:

Since version 26.9.1, deleting a user or changing its password immediately invalidates every token already issued to that user, on every server of a cluster. Previously such a token kept working until it expired on its own. This applies however the change is made: the /api/v1/server/users endpoints, the create user / drop user server commands, the Cypher CREATE USER / ALTER USER …​ SET PASSWORD / DROP USER statements, or an edit to server-users.jsonl.

Tokens in a cluster:

Since version 26.10.1 a token issued by one server of a cluster is honoured by every other server, so a load balancer in front of the cluster needs no sticky sessions. The token names the server that issued it, AU-<server name>-<uuid>, where the server name is the value of arcadedb.server.name (any character outside letters, digits, ., and - is replaced with ). A server that receives a token it has never seen asks the issuer once, through the cluster-internal channel protected by the cluster token, and keeps a local copy for the following requests. The copy is confirmed with the issuer periodically (a third of server.httpAuthSessionExpireTimeout, at most every 60 seconds), which also counts as activity on the issuer, so a token in use anywhere in the cluster does not expire at its source. A logout served by any server drops the copies on every other server. If the issuer no longer holds the session (it expired there, or the server restarted) the copies are dropped at their next confirmation; if the issuer cannot be reached, a copy is served for at most one idle timeout and then dropped.

Every server must be able to map the name in the token to a member of the cluster. This is the same rule a server uses to find itself in arcadedb.ha.serverList: a name@host:port entry, a host equal to the server name, or a -N / _N suffix giving the position in the list (the Kubernetes Helm chart satisfies it as-is). A token whose name matches no member is refused without asking anyone. Tokens in the previous AU-<uuid> form are still accepted, but only by the server that issued them.

API tokens (at- prefix) work the same way on every server: since 26.10.1 they are replicated to the whole cluster, like users and groups (see Cluster security documents).

Number of concurrent tokens:

Since version 26.9.1 the server bounds how many authentication tokens it keeps. A single user may hold up to server.httpAuthSessionMaxPerUser tokens (default 100); past that, its own oldest token is invalidated to make room for the new one, and no other user is ever affected. Across all users the server keeps up to server.httpAuthSessionMax tokens (default 10000); expired tokens are reclaimed first, and a login that still finds no room is answered 503. Set either setting to 0 for the previous unlimited behaviour (not recommended: an unbounded number of sessions is a memory-exhaustion risk).

The client metadata stored with a session (User-Agent, CF-Connecting-IP, X-Forwarded-For, CF-IPCountry, CF-IPCity) is truncated to 256 characters.

Server-Side Transactions

ArcadeDB implements server-side transaction over HTTP stateless protocol by using sessions. A session is created with the /begin request and returns a session id in the response header (example arcadedb-session-id: AS-ee056170-dc9b-4956-8d71-d7cfa01900d4). Use the session id in the request header of further commands you want to execute in the same transaction and request /commit to commit the server side transaction or /rollback to rollback the changes. After a period of inactivity (default is 5 seconds, see server.httpSessionExpireTimeout), the server automatically rolls back and purges expired transactions.

If a request names a session id the server can no longer resolve - because it expired, or was already committed or rolled back - a write is refused with 404, while a read still answers, running outside any transaction. A read that degraded this way says so: the response carries an arcadedb-session-expired header holding the id that could not be resolved, so a client can tell it apart from a read that ran inside its transaction. This applies to GET /query, /begin, /commit, /rollback and the time-series and Prometheus read endpoints.

The time-series and observability endpoints accept a session id so they run under that session’s user and keep it from expiring while you use it, but they do not take part in its transaction: samples are durable as soon as they are written, and the reads only read. Since 26.10.1 an error on one of them therefore leaves your open transaction exactly as it was. Before, it rolled the transaction back while leaving the session alive, so a later /commit reported success and the work was silently gone. The endpoints this covers:

  • the three time-series endpoints, /api/v1/ts/{database}/write, /query and /latest;

  • the PromQL endpoints under /api/v1/ts/{database}/prom, and the Prometheus remote_read and remote_write endpoints;

  • the Grafana query, metadata and health endpoints.

The two errors easiest to hit on the observability routes are a malformed start/end timestamp and a read whose answer exceeds arcadedb.server.maxResultRows; neither touches your transaction any more.

Since v26.10.2 the same holds for GET /api/v1/query and POST /api/v1/query. They still run inside the session’s transaction, so they see what you wrote in it, but they only read, so an error on them leaves your open transaction as it was: a 413 for an answer over arcadedb.server.maxResultRows, or a 400 for a request refused before the query ran (for example Accept: application/x-ndjson on a statement that cannot be streamed). POST /api/v1/command also leaves the transaction alone for a 400 of this kind, which includes a malformed request body: a missing command, a field of the wrong type (such as a numeric language), a limit that is not an integer, or a $bytes/$int8 parameter marker that cannot be decoded. An error raised by the statement itself on /api/v1/command still rolls the transaction back, as described under Commit a transaction.

Transaction Scripts

In case SQL (sql) is supposed to be used as language for a transactions, the language variant SQL Script (sqlscript) is also available. A sqlscript can consist of one or multiple SQL statements, which is collectively treated as a transaction. Hence, for such a batch of SQL statements, no begin and commit commands are necessary, since begin and commit implicitly enclose any sqlscript command.

Streaming Change Events

This feature only works on the server where changes are executed. In a replicated environment, the changes executed on other servers would not fire events on all the servers in the cluster, but only on the local server. The cluster support is coming soon.

The Java API supports real-time change notifications, which the HTTP API implements via a websocket. You can opt into notifications for all changes that occur on a database, or filter by the operation (i.e. create, update, delete) or underlying entity type.

To connect, point your favorite WebSocket client to the ws://SERVER:PORT/ws endpoint. You will need to authenticate with HTTP Basic, which for some clients (like most browsers) is only possible via the URI, like this: ws://USERNAME:PASSWORD@SERVER:PORT/ws. Others will require that you set the Authorization header directly. Check the documentation for your client of choice for details.

To subscribe/unsubscribe to change events, send JSON messages using the following structure:

Property Required Description

action

Required

subscribe or unsubscribe.

database

Required

The database name.

type

Optional

The entity type to filter by.

changeTypes

Optional

Array of change types you’d like to receive. Must be create, update, or delete.

Example: to subscribe to all changes (create, update, delete) for the type Movie in the database movies, use:

{"action": "subscribe", "database": "movies", "type": "Movie"}

If instead, you only want updates, send:

{"action": "subscribe", "database": "movies", "type": "Movie", "changeTypes": ["update"]}

If you want every change on the database (use with caution!):

{"action": "subscribe", "database": "movies"}

Once subscribed, you will get JSON messages for any matching changes with the following properties:

Property Description

database

The source database.

changeType

create, update or delete.

record

The full record that generated the change event.

A subscription is not permanent. Starting from v26.9.1, the server re-checks on every event that the connected user is still allowed to read the database: if that access is revoked, or the user is deleted, the subscription is dropped and the connection closed instead of continuing to stream.

A subscription is to the database NAME. Starting from v26.11.1, it follows the database the server serves under that name: after close database and open database, a restore, an HA snapshot install, or a drop followed by a create of the same name, the existing subscribers receive the changes of the database opened again, with no new subscribe. While the database is closed, nothing is sent. Before v26.11.1 a subscription made before a close database stayed silent after the open database, and so did every new one, until the last subscriber of the name had left.

A subscriber is also expected to keep reading. Events are sent asynchronously, so a client that stops consuming them accumulates unsent frames in the server’s memory. Once more than server.eventBusMaxPendingBytes (16 MB by default) is outstanding towards one connection, that subscription is dropped and the connection closed. Set it to 0 to disable the limit.

In production server mode an error frame answering a failed action carries "The request failed. Check the server log for the details" instead of the engine’s message, which is written to the server log (since v26.10.2). See Error details in production mode.

Streaming Inserts over WebSocket

Starting from v26.10.1, the same ws://SERVER:PORT/ws connection can run a duplex insert session: the client sends records in chunks and gets one acknowledgement per chunk while it is still sending the next, and it decides when the session commits. This is the WebSocket counterpart of the gRPC InsertBidirectional RPC. Each frame is a JSON object whose action is one of:

Action Description

start

Opens a session on database. Optional sessionId (the server generates one when absent) and options, see below. Answered with started, which echoes the modes the session runs under.

chunk

sessionId, chunkSeq (contiguous from 1; re-sending an applied chunk is acknowledged as a replay and not applied again) and records, an array of JSON objects. A record takes its type from @class or from the session’s targetType; an edge names its endpoints with @from and @to (out / in are accepted too). Answered with batchAck.

commit, rollback

Ends the session. Answered with committed, whose outcome says which of the two it was and whose summary carries the totals of the whole session.

The options object of start accepts:

Option Description

targetType

Default type of the records, overridable per record with @class.

transactionMode

per_stream (default, also spelled per_request): one transaction for the whole session, committed or rolled back by the client. per_batch: one transaction per chunk, committed before its acknowledgement. per_row: one transaction per record. Under the last two, committed reports partialCommit: true because a rollback cannot take back what was already committed.

conflictMode

What to do with a record whose key is already taken: error (default) reports it as a failed row with the error code CONFLICT; update rewrites the existing record with the incoming values; ignore drops it; abort is accepted for parity with gRPC and behaves as error. The gRPC spellings (CONFLICT_UPDATE, …​) are accepted as well.

keyColumns

The properties an existing record is matched on. Required for update. When absent, a duplicate is found by the engine where the transaction commits: per row under per_row, per chunk under per_batch, and on the commit frame under per_stream, which is then answered with an error and closes the session.

updateColumnsOnConflict

With update, the only properties overwritten. When absent, every non-key property sent is merged; a null value never overwrites, and an edge’s endpoints are never changed.

validateOnly

true to parse and count every record without writing anything.

The batchAck frame carries chunkSeq, the counters received, inserted, updated, ignored, failed, and an errors array with rowIndex, code, message and exception for each record that could not be applied; the rest of the chunk still goes in. An entry with rowIndex -1 reports a failure of the chunk’s transaction rather than of one record: nothing in the chunk was applied. A session the client walks away from - the connection drops, or no frame arrives for server.wsInsertSessionExpireTimeout milliseconds - is rolled back, and the idle case is reported with an unsolicited error frame.

The code of an errors entry is one of:

Code Meaning

CONFLICT

The record’s key is already taken (conflictMode error or abort), or the commit of the chunk found a duplicated key.

NOT_LEADER

(Since v26.10.2.) The node is a follower of a replicated cluster and refused the write. The entry carries a leaderAddress field with the address of the leader to send the load to. In v26.10.1 and earlier this case was reported as DB_ERROR, without the address.

DB_ERROR

Any other failure.

leaderAddress is a field of its own, outside message, so production mode does not hide it. A per_stream session whose commit frame is refused by a follower is answered with an error frame whose error is "Not the leader" and which carries leaderAddress too (since v26.10.2).

Terminating a chunk. (Since v26.10.2.) While a chunk runs it is listed by the list queries server command, in every transaction mode, and terminate query stops it. The termination undoes what the session’s mode allows: under per_stream the whole session’s transaction is rolled back, chunks acknowledged before included; under per_batch the running chunk’s transaction is rolled back and chunks acknowledged before stay committed; under per_row the rows committed before the termination stay and the rest are not applied. A session joined to an HTTP transaction (transactionMode none) rolls that transaction back and ends it. In every case the session is closed and the server sends an error frame that says what became of the chunk; the connection stays open and can start a new session.

Security. (Since v26.10.2.) The user that opened the connection is checked again on every frame, not only at the handshake:

  • a user that was dropped or given a new password has its session rolled back, receives an error frame, and the connection is closed (close code 1008, "Principal is no longer valid");

  • a user whose grant on the session’s database was revoked has its chunk and commit frames refused and the session rolled back; a rollback frame is always accepted;

  • a session joined to an HTTP transaction (transactionId) that a failed command has rolled back is refused, instead of writing its records outside the transaction, where they would autocommit.

The counters are what the engine wrote, not what the session handed it. A record that a BeforeRecordCreateListener or a BeforeRecordUpdateListener vetoed was refused on purpose, so it is neither inserted or updated nor failed: it is counted as ignored, the same way the gRPC loader counts it. Under conflictMode: update a vetoed update does not fall through to inserting a second record for the key. A client that resumes a load from these counters can therefore trust inserted + updated to be the number of records that are durable.

Before v26.11.1 a record a listener vetoed was counted as inserted or updated, and a vetoed edge was answered as a DB_ERROR row carrying the text of a NullPointerException. A load that was driven off the counters may have marked a chunk as done although the engine had refused some of its records; compare inserted + updated with the number of records the chunk was expected to write.
In production server mode the engine’s text in an error frame and in the message of an errors entry (for a conflict, the key values the row carried) is replaced by "The request failed. Check the server log for the details", and the detail is written to the server log. The code and the exception class are kept. Text the protocol writes about the request itself, such as a missing keyColumns, an invalid option or an expired session, stays visible. Before v26.10.2 the message was sent as is in every mode. See Error details in production mode.

A /ws connection is allowed server.wsMaxControlFrameSize (64 KB by default) per text frame until it has an insert session, and server.wsMaxInsertFrameSize (16 MB) while it has one, so a chunk can carry a whole batch while a connection that never inserts stays on the small budget. A frame over the budget in force closes the connection with a 1009 close code. A separate cap, server.wsMaxInsertChunkRows (100,000), bounds the number of records in one chunk after parsing; a chunk over it is refused with an error frame and leaves the session open, so the client can split the batch and resend it under the same chunkSeq.

Example, upserting people by email:

{"action": "start", "database": "crm", "options": {"targetType": "Person", "conflictMode": "update", "keyColumns": ["email"]}}
{"action": "chunk", "sessionId": "...", "chunkSeq": 1, "records": [{"email": "[email protected]", "name": "Ann"}, {"email": "[email protected]", "name": "Bob"}]}
{"action": "commit", "sessionId": "..."}

Responses

The server answers a HTTP request with a response. This response can have a body, which will always be in the JSON format. Generally, a successful response (HTTP status codes 2xx) contains a result field, while an erroneous request (HTTP status code 4xx) has an error field, and a server error (HTTP status code 5xx), in addition to the error field, provides a detail and an exception field.

A request body that is sent but cannot be parsed as JSON (a truncated object, a scalar such as 123, text that is not JSON) is refused with a 400 and "error": "Invalid JSON payload", whichever route it was sent to. The detail names where the parser stopped, for example End of input at line 1 column 13; it never repeats the body. (Since v26.11.1: before, a body that could not be parsed was treated by most routes as if no body had been sent, and POST /api/v1/server answered a 500. The cluster routes that took a body as optional, such as the security seed and the bootstrap state, now refuse an unparseable one too.) A blank body is no body, and is not refused. The routes whose body is not JSON keep their format: the time series line protocol write, the Prometheus remote_read and remote_write, and the batch import. The MCP endpoint is the exception to the refusal: it answers the JSON-RPC parse error -32700 over HTTP 200.

A route that answers GET also answers HEAD, with the status and the headers of the GET and no body, as RFC 9110 requires, so a container healthcheck such as wget --spider works against any of them. A HEAD runs the GET handler in full and the connector discards the body, so it costs what the GET costs: a probe should target a cheap route such as /api/v1/ready. A request with a method the route does not answer is refused with a 405 whose Allow header names the methods it does answer, for example Allow: GET, HEAD on /api/v1/databases. (Since v26.11.1: before, only /api/v1/ready and /api/v1/health answered HEAD, and a 405 from a core route carried no Allow header, issue #10531.)

Retrying Requests Safely (X-Request-Id)

Every request can carry an X-Request-Id header. The server echoes it on the response and logs it with the request (it generates one when the request has none), which is what correlates a failure with the server log.

On a POST it also makes a retry safe to send verbatim. A successful (2xx) response is kept for arcadedb.ha.idempotencyCacheTtlMs milliseconds, keyed by the id together with the method, path, database and body, and bound to the authenticated user. An identical retry is answered from it instead of executing the request a second time, so a client that lost the answer to a write, a restore or an import can simply send it again with the same id.

  • A failed request is not kept: its retry executes afresh.

  • While the first request is still executing, an identical retry waits briefly for it and then answers 409 Conflict with a Retry-After header, naming RequestStillInFlightException. The request was not executed again: retry it later with the same id to receive the result of the execution in progress.

  • A restore database, restore backup or import database asked for as an SSE progress stream (Accept: text/event-stream) is replayed as a one-event stream carrying its completed event, since a finished stream cannot be sent again. A retry without that header receives the JSON answer instead, whichever way the first request asked.

  • Not replayed: a request inside a client-managed transaction (it carries arcadedb-session-id), a request asking for an NDJSON stream, a response larger than arcadedb.ha.idempotencyCacheMaxBodyBytes bytes, and the bulk-load route POST /api/v1/batch/{database}, whose body is streamed rather than buffered and so cannot be part of the key.

Every response carries the header X-ArcadeDB-Replay-Protection: true, which tells a client that this server answers a repeated id from that cache. The remote Java driver (RemoteDatabase, RemoteServer) sends one X-Request-Id per command call, outside a transaction, and once it has seen that header it sends the same call again, with the same id, to the same server when the response was lost, instead of failing with "the server may already have applied it". The retry names the server process in X-ArcadeDB-Replay-Instance: a server that restarted, or that no longer holds the answer (too large to keep, expired or evicted), answers 412 and does not run the request again. (Since v26.10.1.)

Use a new id for every distinct request. The OpenAPI document declares the header and the 409 on every POST operation that honours them.

Tutorial

Let’s first create an empty database "school" on the server:

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "create database school"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Now let’s create the type "Class":

$ curl -X POST http://localhost:2480/api/v1/command/school \
       -d '{"language": "sql", "command": "create document type Class"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

We could insert our first Class by using SQL:

$ curl -X POST http://localhost:2480/api/v1/command/school \
       -d '{"language": "sql", "command": "insert into Class set name = '\''English'\'', location =  '\''3rd floor'\''"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Or better, using parameters with SQL:

$ curl -X POST http://localhost:2480/api/v1/command/school \
       -d '{"language": "sql", "command": "insert into Class set name = :name, location = :location", "params": {"name": "English", "location": "3rd floor"}}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Reference

Get OpenAPI Definition (GET)

Returns a JSON document containing the HTTP API’s OpenAPI definition.

URL Syntax: /api/v1/openapi.json

Response:

Example:

$ curl -X GET "http://localhost:2480/api/v1/openapi.json"

Return:

{"openapi":"3.0.3", ... }

Check if server is ready (GET, HEAD)

Returns a header-only (no content) status about if the ArcadeDB server is ready.

URL Syntax: /api/v1/ready

This endpoint accepts GET or HEAD requests without authentication, and is useful for remote monitoring of server readiness. Use it as a Kubernetes readiness probe. HEAD is supported so tools that check health with a HEAD request (e.g. wget --spider) work out of the box.

Responses:

  • 204 OK — server is ONLINE

  • 503 server not started yet

When arcadedb.server.readinessRequiresHA=true and HA is active, readiness additionally requires the node to have joined the Raft group: it returns 503 (Node has not yet joined the Raft group) until a leader has been elected. Once a leader is known, a second gate requires the node to be a member of the current Raft configuration and, for a follower, to have replayed the committed log to within arcadedb.server.readinessHAMaxLag entries of the commit index; until then it returns 503 (Node is not yet in the Raft configuration or has not caught up). This keeps a (re)joined follower with a wiped or lagging log out of traffic during a rolling restart, so the write quorum is not dropped. The default (false for readinessRequiresHA) preserves the behavior above. See Health probes.

Example:

$ curl -I -X GET "http://localhost:2480/api/v1/ready"

Return:

HTTP/1.1 204 OK

Check server liveness (GET, HEAD)

Returns a header-only (no content) liveness status. Use it as a Kubernetes liveness probe.

URL Syntax: /api/v1/health

This endpoint accepts GET or HEAD requests without authentication and performs no database I/O. It returns 204 whenever the server process and HTTP layer are up; unlike readiness it never reports a busy state, so a warming-up node is not restarted. The one exception: on a High Availability cluster, a node that has given up recovering from a crash loop returns 503, so the orchestrator restarts it. See Health probes.

Response:

  • 204 OK — server process and HTTP layer are up

  • 503 Service Unavailable — (HA only) the node is stuck in an unrecoverable crash loop and needs a restart

Example:

$ curl -I -X GET "http://localhost:2480/api/v1/health"

Return:

HTTP/1.1 204 OK

Get server information (GET)

Returns the current configuration.

URL Syntax: /api/v1/server

The following mode query parameter values are available:

  • basic returns minimal server information.

  • default returns full server configuration (default value when no parameter is given).

  • cluster returns cluster layout.

In default mode the settings array reports, for each server-level setting, the value this server is actually running on in value and the built-in default in default, with overridden telling you whether the two differ because the setting was given an explicit value - in the server configuration file, by an embedding application, or at runtime with set server setting. Secrets are masked: settings that are secrets in their entirety (the HA cluster token, any key containing password) report , and arcadedb.server.defaultDatabases, whose value merely *embeds credentials, keeps its database names, user names and groups while each password is replaced (since v26.10.1).

Responses:

  • 200 OK

  • 403 invalid credentials

Example:

$ curl -X GET "http://localhost:2480/api/v1/server?mode=basic" \
       --user root:arcadedb-password

Return:

{"version": "26.10.2", "serverName": "ArcadeDB_0"}

Send server command (POST)

Sends control commands to server.

URL Syntax: /api/v1/server

The following commands are available:

  • list databases returns the list of databases installed in the server

  • create database <dbname> creates database with name dbname

  • drop database <dbname> deletes database with name dbname

  • open database <dbname> opens database with name dbname

  • close database <dbname> closes database with name dbname. Refused while a backup, restore, import, export or another close of the same database is running, so it cannot pull the database out from under one; retry once that operation finishes

  • create user { "name": "<username>", "password": "<password>", "databases": { "<dbname>": "admin", "<dbname>": "admin" } } creates user credentials username and password and admin access to databases dbname.

  • drop user <username> deletes user username

  • get server events [<filename>] returns a list of server events, optionally a filename of the form server-event-log-yyyymmdd-HHMMSS.INDEX.jsonl (where INDEX is a integer, i.e. 0) can be given to retrieve older event logs

  • shutdown [<serverName>] kills a server gracefully: this one when no name is given, otherwise the named peer of the HA cluster. See Shutdown server.

  • set server setting <key> <value> sets the server setting with key to value, see the list of server-level settings

  • set database setting <dbname> <key> <value> sets the database’s <dbname> with key to value, see the list of database-level settings

  • connect cluster <address> connects this server to a cluster with address

  • disconnect cluster disconnects this server from a cluster

  • align database <dbname> aligns database <dbname>, see the associated SQL command

  • restore database <dbname> <url> restores a database from a backup file. The URL can be a local path (file://), HTTP/HTTPS URL, or classpath resource. The database must not already exist. See how-to/operations/restore.adoc#restore.

  • import database <dbname> <url> creates a new database and imports data from the given URL (OrientDB export, GraphML, GraphSON, CSV, JSON). Combines create database and IMPORT DATABASE in a single command. See how-to/migration/importer.adoc#importer.

  • list queries [database <dbname>] [tag <label>] lists the statements the server is running. See List and terminate running queries.

  • terminate query <id> and terminate queries tag <label> stop running statements and say whether they stopped. See List and terminate running queries.

  • list transactions lists the open transaction sessions, and terminate transaction <sessionId> rolls one back and ends it. See List and terminate running queries.

Both restore database and import database support SSE progress streaming: send Accept: text/event-stream in the request headers to receive real-time progress events instead of waiting for the response. With an X-Request-Id, a retry of a restore or import that completed is replayed rather than run again, in either encoding (see Retrying Requests Safely (X-Request-Id)).

Only root users can run these command, except the list databases command, which every user can run, and this user’s accessible databases are listed, and the commands on running work (list queries, terminate query, terminate queries, list transactions, terminate transaction), which every user can run on their own work.

Responses:

  • 200 OK

  • 400 invalid command

  • 403 invalid credentials

  • 404 the command names a database this server does not have

  • 503 the database is there and cannot serve the command now: closed by close database, being restored, being dropped, or being recovered by HA (see a database that is not available)

  • 409 the name is taken: create database on an existing database ("error": "Database already exists") or create user on an existing user ("error": "User already exists") (Since v26.10.2)

  • 500 invalid JSON request body

Since 26.10.1, a command naming a database that does not exist - open database nosuchdb, for example - answers 404 instead of 500. Naming a database the server has not got is a mistake in the request, not a server fault. Adjust any monitoring that alerts on 5xx from this endpoint. Since 26.11.1 a database that is there and held - closed, being restored or dropped - answers 503, not 404 (see a database that is not available).

Databases opened on demand. Since v26.11.1 a database that exists on disk but is not open on the server, because arcadedb.server.databaseLoadAtStartup is false, is opened by the first HTTP request that names it (query, command, batch load, WebSocket insert session), as the Postgres, BOLT and gRPC protocols already did. Until v26.10.2 the HTTP API answered Database '<dbname>' is not available until the database was opened explicitly with open database. Only a directory that really holds a database (one with a schema.json or schema.prev.json file) is opened: any other name is refused as before, and a request never creates a directory. The user must still be authorized on the database. This makes databaseLoadAtStartup=false practical for a server that hosts many databases: it starts without opening any of them, and each one is opened by its first request (issue #10234).

A database closed with the close database server command is the exception: it was closed on purpose, for a restore of its files or a cold backup, so an HTTP request does not reopen it. It stays closed until open database (or a restore, an import or a drop of that name). The server remembers this in memory only and for the node that received the command: after a restart the database is opened again, at start or on its first request.

The same rule holds for the other protocols. A database that a close database command closed, or that an overwrite restore of its name is about to replace, is not opened by a Postgres, BOLT, gRPC, Redis or MongoDB connection, or by an MCP or Gremlin request, that names it: the client is told that the database is not available, as an HTTP request is (503 for HTTP, SQLSTATE 57P03 for Postgres, status UNAVAILABLE for gRPC, the transient Neo.TransientError.Database.DatabaseUnavailable for BOLT, a LOADING error reply for Redis, error 365 TemporarilyUnavailable for MongoDB; an engine error category of its own, UNAVAILABLE, so that none of them reports it as a server fault). A Redis key <database>.<key> whose database is held is refused the same way, and not kept as a plain key of the connection. (Since v26.11.1: before, these protocols opened it, so a cold backup copied a database that was open and being written to, and the writes a client made during an overwrite restore were acknowledged and then discarded when the restored copy replaced the database, issue #10533.) A database that is already open keeps serving reads while an overwrite restore replaces it, and refuses every write until the restore ends, on every protocol and also on a connection opened before the restore started: the write is answered as "not available", with the codes above, and a database setting change is refused the same way. The restore first waits for the commits already running. (Since v26.11.1: before, those writes were acknowledged and then lost with the database the restore replaced, issue #10689.) A name that is not a database at all is the client’s to correct, and every protocol says so rather than reporting a server fault: SQLSTATE 3D000 for Postgres, status NOT_FOUND for gRPC, Neo.ClientError.Database.DatabaseNotFound for BOLT, error 26 NamespaceNotFound for MongoDB, ERR Database '<name>' not found for Redis, 404 for HTTP (an engine error category of its own, DATABASE_NOT_FOUND, since v26.11.1, issue #10795). A connection that keeps naming a database across a close database + open database, a drop database + create database or an overwrite restore is served by the new database as soon as it is there (issue #10794).

Since v26.11.1, HTTP tells the two apart as every other protocol does (issue #10872): a name that is no database answers 404 with "error": "Database not found" and the exception com.arcadedb.exception.DatabaseNotFoundException; a database that is there and cannot serve the request now answers 503 with the exception com.arcadedb.exception.DatabaseNotAvailableException, an error naming the state, and the state in exceptionArgs, so a client is told it in production mode too, where the detail is not sent:

exceptionArgs error when

CLOSED

Database is closed

a close database command closed it; it stays closed until open database

RESTORING

Database is being restored

an overwrite restore holds its name and the database is not open

WRITES_REFUSED

Database does not accept writes

an overwrite restore holds it while it is open: reads still answer 200

DROPPING

Database is being dropped

a drop database is deleting its files

RECOVERING

Database is being recovered

on a replicated cluster, a snapshot install or a resync has to reconcile this node’s copy first

UNAVAILABLE

Database is not available

any other reason it is not open for this request, such as a server that is starting or stopping

No Retry-After is sent: how long an operator keeps a database closed is not something the server knows. The Java remote client rebuilds both exceptions, with the state as DatabaseNotAvailableException.getReason(), and does not retry either of them.

Before v26.11.1 these states answered 404 Database not found, the answer of a name that is no database, with the same exception class, so the two bodies were identical in production mode. A client or a monitor that treated a 404 from a database endpoint as "the database does not exist" now sees 503 for a database that exists and is held.
An opened database stays open until the server stops or close database closes it, and there is no limit on how many the requests can open: each one holds its data files, its caches and, once written, a WAL file (since v26.11.1 a database holds no thread of its own and no WAL file until it is written, and a WAL file nobody writes for arcadedb.txWalIdleTimeout is closed). Size the memory and the file descriptor limit of a server that hosts thousands of databases for the ones in use at the same time. Closing the databases that fall idle is not part of this behaviour.

Examples:

List databases

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "list databases"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": ["school", "mydatabase"]}

Create database

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "create database mydatabase"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

If a database with that name already exists, the command fails with 409 and "error": "Database already exists"; the label stays readable in production mode, where detail is concealed. (Since v26.10.2) In 26.10.1 and earlier it answered 400. Restoring a backup into a name that is taken (restore database, or restore backup without overwrite) still answers 400.

Drop database

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "drop database mydatabase"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

Open database

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "open database mydatabase"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

Close database

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "close database mydatabase"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

Restore database

Restores a database from a backup file (local or remote). This is the fastest way to provision a database from a backup archive.

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "restore database mydatabase https://example.com/backups/mydatabase.zip"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Restoring from a local file:

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "restore database mydatabase file:///path/to/mydatabase-backup.zip"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

Create user

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "create user {\"name\": \"myuser\", \"password\": \"mypassword\", \"databases\": {\"mydatabase\": \"admin\"}}"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

If a user with that name already exists, the command fails with 409 and "error": "User already exists", on a single server and on a cluster alike; POST /api/v1/server/users answers the same. (Since v26.10.2) In 26.10.1 and earlier it answered 403 with "error": "Security error", the status a non-root caller gets, so the two could only be told apart by the message text.

Drop user

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "drop user myuser"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

Shutdown server

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "shutdown"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

Shutting down another node of an HA cluster

Naming a server after the command stops that peer instead of the one receiving the request. The command is relayed over the cluster’s own transport — encrypted when the cluster has TLS configured for its side channels, see TLS/SSL for peer-to-peer transfers:

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "shutdown arcadedb-1"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

The name is matched as a substring against each peer’s identifier and its HTTP address, so a partial name is enough:

  • No peer matches it — the request fails with Cannot find server '<name>' in the cluster.

  • More than one peer matches it — the request is refused rather than resolved to an arbitrary one. arcadedb-1 matches arcadedb-10 as well, so on a cluster holding both, name the one you mean exactly.

  • The name is this server — this server shuts down, exactly as the plain shutdown command does.

In a Kubernetes StatefulSet each pod’s hostname is the name to use (arcadedb-0, arcadedb-1, …). Prefer POST /api/v1/cluster/leave when the intent is to take a node out of the cluster rather than to stop its process.

Get server events

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "get server events"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": [{"time": "2023-06-18 15:37:40.378", "type": "INFO", "component": "Server", "message": "ArcadeDB Server started in \u0027development\u0027 mode (CPUs\u003d8 MAXRAM\u003d4,00GB)"}]}

Set server setting

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "set server setting arcadedb.server.name player0"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

Set database setting

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "set database setting mydb arcadedb.typeDefaultBuckets 4"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

Connect cluster

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "connect cluster 192.168.0.1"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}
This adds the given server to this server’s cluster; it does not make this server join another cluster. (Since v26.10.1) An address that resolves to this server itself is refused with 400 instead of being silently accepted as a no-op.

Disconnect cluster

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "disconnect cluster"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

Align database

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "align database mydb"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{"result": "ok"}

List and terminate running queries

Since 26.10.2 a client can see what the server is running, stop one statement, and verify afterwards that the server really stopped working on it. A benchmark harness or a coordinator that gives up on a query (its own deadline, a killed client process) uses this to free the server before it runs the next one.

The list covers every protocol, not only HTTP: statements received over PostgreSQL, Bolt, gRPC, MongoDB, Redis, the Gremlin Server and MCP (see reference/mcp/mcp.adoc#mcp-running-tool-calls) are listed and stopped the same way, and the same registry answers the Cypher SHOW TRANSACTIONS and TERMINATE TRANSACTIONS commands and the Studio Running Queries tab.

Label your work. Send the X-ArcadeDB-Query-Tag header with a query, a command, a GET query or a batch load. The value is an opaque label of at most 256 printable characters; the statement is listed under it. Sent to /api/v1/begin, it labels every statement of that transaction session that carries no label of its own. A value that breaks these rules is refused with 400 Bad Request on every endpoint, before anything runs or a transaction session is created. (Since v26.10.2: a refused tag on /api/v1/begin still opened a transaction session that no client could end.)

$ curl -X POST http://localhost:2480/api/v1/command/mydb \
       -H "X-ArcadeDB-Query-Tag: bench-run-42" \
       -d '{"language": "opencypher", "command": "UNWIND range(1, 10000) AS i UNWIND range(1, 10000) AS j RETURN sum(sin(toFloat(i * j))) AS total"}' \
       --user root:arcadedb-password

List. list queries returns the statements this server is running. database <dbname> and tag <label> narrow the list (the label runs to the end of the command):

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "list queries tag bench-run-42"}' \
       --user root:arcadedb-password
{"result": [{"id": "q118-5e1f0a9c", "server": "ArcadeDB_0", "database": "mydb", "user": "root", "protocol": "http",
  "language": "opencypher", "text": "UNWIND range(1, 10000) AS i UNWIND ...", "tag": "bench-run-42",
  "startedAt": 1791620000000, "elapsedMs": 2311, "terminating": false}]}

An id is q<n>-<instance>: the instance part belongs to the server that issued it, so a terminate query that a load balancer sends to another server answers not found there rather than stopping a different statement. The text is truncated to 1024 characters, and the parameters are never listed. protocol names how the statement reached the server: http, postgres, bolt, grpc, mongodb, redis, gremlin (the Gremlin Server), mcp (a tool call of the MCP server) or ws (a chunk of a WebSocket insert session). Some entries carry more fields:

Field Present when

sessionId

The statement runs in an HTTP transaction session.

connectionId

The protocol keeps connections of its own: the backend process id of a PostgreSQL connection (what pg_backend_pid() answers, see Cancelling a statement), bolt-<n> for a Bolt connection, the channel id of a MongoDB or Gremlin Server connection, the connection name of a Redis connection, the session id of a WebSocket insert session.

clientAddress

The address of the client the statement came from, on every protocol that has one: /host:port. An MCP tool call made over the stdio transport lists stdio. Absent when the transport cannot report the client, for example a Gremlin Server session or a custom Gremlin channelizer.

forwardedFrom

On an HA leader, for a statement a replica forwarded to it: the id the statement has on that replica.

forwardedTo

On an HA replica, for a statement it forwarded to the leader: the name of the leader running it.

Terminate. terminate query <id> stops a statement, and terminate queries tag <label> stops every statement carrying the label that you may see - for root, every user’s statement carrying it. The command waits for the work to end before it answers, up to wait milliseconds in the request (5000 when absent, 10000 at most; poll list queries for longer), and says what happened:

$ curl -X POST http://localhost:2480/api/v1/server \
       -d '{"command": "terminate query q118-5e1f0a9c", "wait": 2000}' \
       --user root:arcadedb-password
{"result": {"id": "q118-5e1f0a9c", "status": "terminated"}}
Status Meaning

terminated

The statement stopped, and what it wrote was rolled back.

completed

The statement ended on its own before it noticed the request: what it did stands, a write it committed included.

terminating

The statement was asked to stop and has not reached a check yet. Poll list queries until it is gone.

not found

No such statement is running: it already ended, or it is not one you may see.

A statement that ran in a transaction session (/api/v1/begin) answers two more fields, in the answer of terminate query and in each entry of the answer of terminate queries tag (since v26.10.2): sessionId, the session it ran in, and sessionOpen, whether that session is still open now that the statement has ended. A terminated command ends its session (false). A terminated read leaves the session to its client (true), as described under Transaction sessions below: a caller that wants the session gone, not only the statement, ends it with terminate transaction <sessionId>.

{"result": {"id": "q121-5e1f0a9c", "status": "terminated", "sessionId": "AS-ee056170-dc9b-4956-8d71-d7cfa01900d4", "sessionOpen": true}}

The terminated request fails with HTTP 409 and "error": "Query terminated". It is not a transient error: do not retry it. The Java RemoteDatabase raises it as com.arcadedb.exception.QueryTerminatedException, which its transaction() does not retry. Once a statement is gone from list queries, its rollback and its cleanup are over: its absence is the evidence that the server no longer works on it.

Each protocol reports the termination to its client in its own terms, never as a transient error:

Protocol The terminated statement fails with

HTTP

Status 409, "error": "Query terminated".

PostgreSQL

SQLSTATE 57014 (query_canceled). The connection stays open.

Bolt

Neo.ClientError.Transaction.Terminated, which drivers do not retry.

gRPC

Status CANCELLED with the trailer arcadedb-exception-class set to com.arcadedb.exception.QueryTerminatedException. The gRPC Java client raises that exception.

MongoDB

Error code 11601 (Interrupted), what MongoDB answers for an operation killOp interrupted.

Redis

An ERR error whose message says the statement was terminated.

/ws insert session

(Since v26.10.2.) An error frame ("error": "Insert session error") saying what became of the chunk. The session is closed and the connection stays open, see Streaming Inserts over WebSocket.

Termination is cooperative: the statement stops at its next check, made by the SQL and openCypher operators that loop over data (scans, expansions, UNWIND, aggregations, sorts, FOREACH, script lines), and its commit is refused if it reaches one. A Gremlin traversal checks inside its adjacency steps (out(), bothE(), …​) and while its results are iterated. A JavaScript script (command("js", …​)), a JavaScript function called from SQL (DEFINE FUNCTION …​ LANGUAGE js) and a JavaScript trigger are interrupted through GraalVM, so even an endless loop in user code stops; the SQL such a script runs is stopped as any other.

Clients that go away. A client that disconnects while its statement runs (a closed socket, a killed process, curl --max-time) terminates the statement, instead of leaving the server busy with it until it completes or reaches arcadedb.command.timeout. This applies to plain HTTP/1.x connections once the request body has been read, not to TLS connections, to HTTP/2, or to a request whose client has already pipelined the next one. A statement that runs in a transaction session and is terminated this way follows the transaction session rule below. Keep-alive and pipelined requests are not affected. While the statement runs, the connection’s read timeout (arcadedb.network.socketTimeout, 30 seconds by default) is suspended, so a client waiting for a statement that runs longer than that still receives its answer; the timeout is restored when the statement ends, and it still closes idle keep-alive connections. HTTP connections use TCP keepalive, so a client that vanishes without closing its connection (a host crash, a network partition) is eventually detected by the operating system and its statement terminated; the operating system’s keepalive timing applies, so set arcadedb.command.timeout to bound such statements sooner. A client that half-closes its side of the connection after sending the request and still waits for the response cannot be told from one that left: for such clients, set arcadedb.server.httpTerminateOnClientDisconnect to false.

Transaction sessions. Terminating a command that writes in a transaction session (/api/v1/command, /api/v1/batch) also ends the session and rolls back everything the session wrote, exactly as a failure of that command does. Terminating a read (GET or POST /api/v1/query) or a time-series, vector, PromQL or Grafana request stops only that request and leaves the session and its transaction as they were, so a client whose read timed out can retry it and still commit its writes (since v26.10.2). The answer of terminate query says which case applied through sessionOpen. list transactions lists the open sessions (id, user, database, tag, idle time, whether a request is running in it, and the ids of its running statements), and terminate transaction <sessionId> terminates what runs in it, rolls it back and ends it. That includes a /ws insert session writing into it, whose current chunk is listed under the session. The command never waits longer than its time limit (at most 10 seconds): when the session is still busy at that point it answers terminating, and the transaction is rolled back as soon as the running work ends. A later /api/v1/commit on that session publishes nothing and answers 204 with the arcadedb-session-expired header, as for any session that no longer exists.

Who can do what. Every authenticated user can run these commands. root sees and stops every statement and session; any other user only their own, and someone else’s is answered not found, so its existence is not given away. The commands are never held back by the query admission gate (arcadedb.queryMaxConcurrent), so a saturated server can still be relieved, and they are never forwarded to the leader: each server lists and stops its own work.

On an HA cluster a write that a replica forwards to the leader runs on the leader. It is listed on both: on the replica with forwardedTo (the leader’s name), on the leader with forwardedFrom (the id it has on the replica) and the client’s tag. terminate query <replica id> sent to the replica stops the work on the leader too, and the same command sent to the leader finds the statement by the replica’s id. See Running statements on a cluster.

Credentials. The listed text masks the value after IDENTIFIED BY, PASSWORD (PASSWD, PWD), TOKEN, SECRET, API_KEY and AUTHORIZATION (also after a Bearer or Basic scheme) with * (best effort).

Cluster authentication session lookup (POST, internal)

(Since v26.10.1) POST /api/v1/cluster/auth-session is a cluster-internal route that lets servers share authentication tokens (see Tokens in a cluster under Token-Based Authentication). It is not meant for clients and is listed here only so it is not mistaken for a public API. The request must arrive through the cluster-token channel (X-ArcadeDB-Cluster-Token header) on behalf of the root user: a request without that header is refused with 403, even with root credentials, and an invalid cluster token gets 401.

The body is {"token": "<AU-…​ token>", "action": "validate" | "revoke"} (action defaults to validate). validate answers 200 with {"user": …​, "createdAt": …​} when this server issued the session, and 404 otherwise (including for a copy held on behalf of another server); revoke drops the session and answers 204. A missing token or an unknown action answers 400.

List Databases (GET)

Returns a list of available databases for the requesting user.

URL Syntax: /api/v1/databases

Responses:

  • 200 OK

  • 403 invalid credentials

Example:

$ curl -X GET http://localhost:2480/api/v1/databases \
       --user root:arcadedb-password

Return:

{"result": ["school", "mydatabase"]}

Does database exist (GET)

Returns boolean answering if database exists.

URL Syntax: /api/v1/exists/{database}

Responses:

  • 200 OK

  • 400 no database passed

Example:

$ curl -X GET http://localhost:2480/api/v1/exists/school \
       --user root:arcadedb-password

Return:

{"result": true}

Get operation progress (GET)

Returns the long-running maintenance operations currently in progress on this server for one database, with their step-by-step progress. CHECK DATABASE, REBUILD INDEX, COMPACT INDEX, BACKUP DATABASE and IMPORT DATABASE publish their progress here while they run. The endpoint answers from an in-memory snapshot without touching the database, so it is safe to poll at any frequency (the console and Studio poll it to render a live progress bar). In a cluster each server reports its own operations.

URL Syntax: /api/v1/progress/{database}

Responses:

  • 200 OK

  • 400 no database passed

  • 403 the user is not authorized on the database

Example:

$ curl -X GET http://localhost:2480/api/v1/progress/school \
       --user root:arcadedb-password

Return (one entry per running operation, empty array when nothing is running):

{"result": [{
  "id": 3,
  "database": "school",
  "operation": "check database fix",
  "stepName": "Checking vertices 'Account'",
  "stepIndex": 3,
  "totalSteps": 9,
  "done": 421000,
  "total": 1000000,
  "percentage": 42,
  "startedOn": 1786370000000,
  "elapsedMs": 154000
}]}

percentage is -1 when the current step’s total is unknown (for example the backup or the import, whose sizes cannot be computed upfront).

Login - Get Authentication Token (POST)

Authenticates a user and returns an authentication token that can be used for subsequent requests instead of sending credentials each time. See Token-Based Authentication for more details.

URL Syntax: /api/v1/login

This endpoint requires HTTP Basic Authentication with valid credentials. On successful authentication, it returns a JSON response containing the authentication token.

Responses:

  • 200 OK - returns token and username

  • 403 invalid credentials

  • 503 the server holds the maximum number of concurrent authentication sessions and none could be reclaimed - see server.httpAuthSessionMax (since v26.9.1)

Example:

$ curl -X POST http://localhost:2480/api/v1/login \
       --user root:arcadedb-password

Return:

{"token": "AU-ArcadeDB_0-2af64e60-8455-423a-bc64-ed0e19729f04", "user": "root"}

The returned token can be used in subsequent requests via the Authorization: Bearer header:

$ curl http://localhost:2480/api/v1/databases \
       -H "Authorization: Bearer AU-ArcadeDB_0-2af64e60-8455-423a-bc64-ed0e19729f04"

Logout - Invalidate Authentication Token (POST)

Invalidates an authentication token so it can no longer be used for requests. The token must be provided via the Authorization: Bearer header.

URL Syntax: /api/v1/logout

Responses:

  • 204 OK - token invalidated (or was already invalid)

Example:

$ curl -X POST http://localhost:2480/api/v1/logout \
       -H "Authorization: Bearer AU-ArcadeDB_0-2af64e60-8455-423a-bc64-ed0e19729f04"

After logout, any attempt to use the invalidated token will result in a 401 Unauthorized response.

List Active Authentication Sessions (GET)

Returns a list of all active authentication sessions. This endpoint is restricted to root users only for security reasons.

URL Syntax: /api/v1/sessions

Responses:

  • 200 OK - returns list of active sessions

  • 403 forbidden (non-root user)

Example:

$ curl http://localhost:2480/api/v1/sessions \
       --user root:arcadedb-password

Return:

{
  "result": [
    {
      "token": "AU-ArcadeDB_0-2af64e60-8455-423a-bc64-ed0e19729f04",
      "user": "root",
      "elapsedMs": 12345
    },
    {
      "token": "AU-6fa459ea-ee8a-3ca4-894e-db77e160355e",
      "user": "admin",
      "elapsedMs": 5678
    }
  ],
  "count": 2
}

The elapsedMs field indicates how many milliseconds have passed since the last request using that token.

Create an API token (POST)

Mints a long-lived API token (at- prefix) for a script or service. Root only. The plaintext token is returned once, in result.token, and is never shown again: the server keeps only its hash. On a cluster the request is forwarded to the leader and the token is replicated to every node. GET /api/v1/server/api-tokens lists the tokens and DELETE /api/v1/server/api-tokens?token=<hash> revokes one. Studio’s API Tokens panel uses the same routes.

URL Syntax: /api/v1/server/api-tokens

Body: name (required), database (default *, every database), expiresAt (epoch milliseconds, 0 for no expiry) and permissions (the same structure as a group).

Responses:

  • 201 created - the token is in result.token

  • 400 missing name or malformed permissions

  • 403 forbidden (non-root user)

  • 409 a token with that name already exists. (Since v26.10.2) the body also names the exception in exception, like the other error bodies

  • 412 the request did not arrive over a protected transport and arcadedb.server.apiTokenRequireSecureTransport is on (since v26.10.1)

$ curl -X POST https://localhost:2490/api/v1/server/api-tokens \
       --user root:arcadedb-password \
       -d '{"name":"etl-job","database":"mydb"}'

Transport check (since v26.10.1). The token authenticates whoever holds it, so the server checks how the mint reached it. The transport counts as protected when the request came over HTTPS, from a loopback address, or from a reverse proxy listed in arcadedb.server.apiTokenTrustedProxies that reports https for every hop through X-Forwarded-Proto or the RFC 7239 Forwarded header. What happens otherwise depends on arcadedb.server.apiTokenRequireSecureTransport:

  • false (the default in 26.10.1): the token is minted and the server logs Minting an API token over an unprotected transport (scheme=…​, peer=…​) at WARNING.

  • true: the mint is refused with 412 and a body explaining the three ways out: connect over TLS, list the proxy, or turn the check off.

On a cluster the check also covers the hop between nodes: a mint forwarded to the leader over cleartext inter-node HTTP is refused with 412 too, even when the client itself used HTTPS.

The default of arcadedb.server.apiTokenRequireSecureTransport is scheduled to change to true in 27.1.1. From that release an unprotected mint is refused unless you set the value back to false. It is false today only because Studio’s own Create Token button uses this route, and enforcing it would break Studio served over plain HTTP from a remote host. If you mint tokens over plain HTTP, plan for HTTPS (or a trusted proxy) before upgrading.

Behind a TLS-terminating reverse proxy. When nginx, Envoy or a Kubernetes ingress terminates TLS and forwards to ArcadeDB over HTTP, the server sees a cleartext request from the proxy’s address. List that address (or its subnet) in arcadedb.server.apiTokenTrustedProxies, for example -Darcadedb.server.apiTokenTrustedProxies=10.0.0.5,10.1.0.0/16, and make sure the proxy sets X-Forwarded-Proto (nginx: proxy_set_header X-Forwarded-Proto $scheme;). The rules are strict on purpose:

  • A peer that is not on the list gains nothing by sending the same headers. They are ignored, because the client asking for a token is the one that would forge them.

  • Entries must be literal addresses or CIDR ranges. A hostname is rejected rather than resolved.

  • A list that does not parse is treated as empty, so a typo denies instead of opening the gate. The server logs the discarded list at SEVERE (Ignoring the whole of arcadedb.server.apiTokenTrustedProxies (…​)).

  • List only an L7 proxy that writes the header itself. An L4 balancer (nginx stream, HAProxy mode tcp) passes the client’s own headers through, so listing one lets any client vouch for itself.

  • If the request carries a Forwarded header, every element must include proto=https; an element without proto= refuses the mint. A proxy that does not use Forwarded should strip any it receives.

The gRPC CreateApiToken RPC applies the same list, reading the x-forwarded-proto and forwarded metadata keys. Unlike HTTP it always refuses an unprotected mint, whatever apiTokenRequireSecureTransport says.

Execute a query (GET|POST)

This command allows executing idempotent commands, like SELECT and MATCH:

URL Syntax GET: /api/v1/query/{database}/{language}/{command}

URL Syntax POST: /api/v1/query/{database}

Where:

  • database is the database name

  • language is the query language used. is the query language used, between "sql", "sqlscript", "graphql", "cypher", "gremlin", "mongo" and any other language supported by ArcadeDB and available at runtime.

  • command the command to execute in encoded format

  • params (optional), is the map of parameters to pass to the query engine via the POST body, where parameters are introduced with a colon :, or a JSON array of positional parameters. Parameter values may include the typed-marker objects described in Typed parameter markers to send byte[] — e.g. INT8-encoded vectors — without a float32 round trip.

When using the GET variant the query needs to be URL encoded.

Due to security reasons (encoded) slashes / (%2F) which are used for divisions or block comments, cannot be used in queries via the GET method with the query/ endpoint.
Question marks (?) cause the server to stop reading the query string when sent via GET. To use question marks (inside strings) one can use format('%c',63); in this case make sure to replace all percent symbols (%) in the format string with %%.

These restrictions do not apply to the POST variant, where the language and command are send in the body.

Even though a POST method is used, the query in command has to be idempotent.

(Since v26.10.2.) A SQL statement is classified by everything it runs, not only by its outermost statement. A write nested in a LET, a projection, a FROM subquery, a CASE, a RETURN or a script block (IF, FOREACH, WHILE) makes the whole statement a write, so /api/v1/query refuses it with 400 (the query is not idempotent) and writes nothing, for example SELECT FROM Account LET $x = (INSERT INTO Log SET a = 1) or SELECT FROM (INSERT INTO Log SET a = 1). Send such a statement to /api/v1/command. A nested read, such as SELECT $c FROM Account LET $c = (SELECT count(*) FROM Account), still runs. In v26.10.1 and earlier these nested writes ran through the query endpoint.

(since v26.10.1) POST /api/v1/query runs without an implicit transaction, like GET /api/v1/query, so a full scan of a large type can use several threads and is as fast as with GET. Inside a session (/begin) a scan stays sequential to keep it consistent with the transaction. POST /api/v1/command still runs in a transaction.

EXPLAIN is answered the same way by the GET and the POST variants and by /api/v1/command: the plan arrives in the explain (indented text) and explainPlan (structured) members of the response envelope, and result is empty — the plan is the answer, not a row. Requesting it with Accept: application/x-ndjson is refused with 400 ("EXPLAIN produces a plan, not a row stream"), because a stream of rows plus a stats trailer has nowhere to carry one. Before 26.10.1 the GET variant reached neither rule: it serialized the plan as an ordinary result row, and streamed it as an ndjson record line that no consumer of that encoding can read.

With Accept: application/x-ndjson the rows are streamed one per line (record), followed by a stats trailer. While the server is still producing the next row it writes an empty line every arcadedb.server.httpStreamingKeepAliveInterval milliseconds (default 5000, 0 disables), so a client that bounds silence does not mistake a slow query for a dead server. Consumers must ignore blank lines, which the Java driver does. The Java driver fails a stream after arcadedb.network.socketTimeout of silence, with a minimum of 30 seconds, as for any other call.

A failure after the stream has started cannot change the 200 status line, so it is reported in-band as a last error line instead of the stats trailer: {"error": {…​}}. It carries what the buffered response would have answered the same failure with:

  • message: the failure’s own message outside production mode. (Since v26.10.2) in production server mode it is the classified label, as the buffered response conceals the raw text;

  • status: the HTTP status the buffered response would have used, and exception / exceptionArgs (the latter only when the failure has structured arguments, such as a duplicated key’s index, keys and RID);

  • (since v26.10.2) error, the classified label (for example Cannot execute command), requestId when the request carried one, detail, the cause chain, outside production mode only, and retryAfter, the integer number of seconds the buffered response would have sent as Retry-After, present only on a retryable refusal (a 503, or a 409 for a request with the same X-Request-Id still in flight).

Key on status, exception and retryAfter rather than parsing message. A stream that ends with neither a stats nor an error line did not arrive complete.

Responses:

  • 200 OK

  • 400 invalid language, invalid query

  • 403 invalid credentials

  • 404 database does not exist (Database not found)

  • 503 the database is there and cannot serve the request now: closed, being restored, dropped or recovered (see a database that is not available)

  • 500 cannot execute query

  • 503 the server is busy and the query can be retried as it is: the running queries hold the heap budget (QueryHeapBudgetExceededException, see queryMaxHeapRAM), or the query admission gate did not start it, because it waited longer than queryQueueTimeout or found the queue full (QueryAdmissionException, see queryMaxConcurrent)

  • 503 the database was closed while the request was running because it is being reinstalled from a snapshot on an HA replica (DatabaseIsReopeningException), or the server is shutting down: retry as it is

  • 503 the database was closed (CLOSE DATABASE, a restore) while the request was running and is still there (DatabaseNotAvailableException, exceptionArgs CLOSED) (since v26.11.1, before it was 404), or 404 it was dropped (DatabaseIsClosedException)

Example:

$ curl -X GET http://localhost:2480/api/v1/query/school/sql/select%20from%20Class \
       --user root:arcadedb-password

The query endpoint may also be used via the POST method, which has no character restrictions such as / or ?:

$ curl -X POST http://localhost:2480/api/v1/query/school \
       -d '{"language": "sql", "command": "select from Class"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

The POST variant accepts additional parameters in the JSON body:

  • autoCommit (optional) controls transaction behavior (same as in Execute database command):

    • true: Forces the query to execute within an atomic transaction

    • false: Disables transaction wrapping (query executes directly)

    • not specified: Uses automatic behavior (no transaction for read-only queries)

Example of forcing a transaction for a query to ensure consistent reads:

$ curl -X POST http://localhost:2480/api/v1/query/mydb \
       -d '{"language": "sql", "command": "SELECT FROM Account WHERE balance > 1000", "autoCommit": true}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Execute database command (POST)

Executes a non-idempotent command (as an implicit transaction).

URL Syntax: /api/v1/command/{database}

Where:

  • database is the database name

Example to create the new document type "Class":

$ curl -X POST http://localhost:2480/api/v1/command/school \
       -d '{"language": "sql", "command": "create document type Class"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

The payload, as a JSON, accepts the following parameters:

  • language is the query language used, between "sql", "sqlscript", "graphql", "cypher", "gremlin", "mongo" and any other language supported by ArcadeDB and available at runtime.

  • command the command to execute in encoded format

  • awaitResponse (optional) a boolean which is by default "true", if set to "false" the command will be executed asynchronously and only acknowledgement of receiving the command is responded. The completion of the command is noted in the log, yet no results of the command can be returned.

  • limit (optional) is the maximum number of results to return

  • params (optional), is the map of parameters to pass to the query engine, where parameters are prefixed with a colon :, or a JSON array of positional parameters. Parameter values may include the typed-marker objects described in Typed parameter markers to send byte[] — e.g. INT8-encoded vectors — without a float32 round trip.

  • retries (optional), is the number of times the command (transaction) is retried. When omitted it follows the arcadedb.txRetries database setting (3 by default), the same attempt count the embedded and the remote Java APIs take it from; up to v26.9.0 it was hard-coded to 1, so nothing was ever retried. Only a concurrent-modification conflict is retried (NeedRetryException and DuplicatedKeyException, the latter capped at a single retry), and always after a full rollback, so a retried attempt starts from committed state with nothing of the failed one surviving. Note the consequence for side effects outside the database: a SQL or JavaScript function that calls another system can now run more than once per request. Send "retries": 1 to keep the previous single-attempt behavior for such a command.

  • autoCommit (optional) controls transaction behavior:

    • true: Forces the command to execute within an atomic transaction that auto-commits on success

    • false: Disables automatic transaction wrapping (command executes directly without transaction)

    • not specified (default): Uses automatic behavior (commands are wrapped in atomic transactions unless a session is active)

    • NOTE: This parameter is ignored when using session-based transactions (see Begin a transaction). If you provide autoCommit: true with an active session, a warning will be logged and the session transaction will be used instead.

  • serializer (optional) specify the serializer used for the result:

    • graph: returns a graph separating vertices from edges

    • record: returns everything as records

    • studio: by default it’s like record but with additional metadata for vertex records, such as the number of outgoing edges in @out property and total incoming edges in @in property. This serializer is used by Studio.

Asynchronous commands (using "awaitResponse": false) are queued, and their processing is configurable via async* database settings.

Responses:

  • 200 OK

  • 202 Accepted

  • 400 invalid language, invalid command

  • 403 invalid credentials

  • 409 (since v26.11.1) the command creates, or renames onto, a schema object name that is already taken: a type, property, index, constraint, bucket, trigger, view or function. error names the kind (for example "Type already exists", kept in production mode), exception is com.arcadedb.exception.AlreadyExistsException and exceptionArgs the kind (TYPE, PROPERTY, INDEX, …​). Not retryable. Before v26.11.1 these answered 500 ("Cannot execute command" or "Internal error"), and a rename onto a taken type name 400

  • 503 the server is busy and the command can be retried as it is: the running queries hold the heap budget (QueryHeapBudgetExceededException, see queryMaxHeapRAM), or the query admission gate did not start it, because it waited longer than queryQueueTimeout or found the queue full (QueryAdmissionException, see queryMaxConcurrent)

  • 503 the database was closed while the request was running because it is being reinstalled from a snapshot on an HA replica (DatabaseIsReopeningException), or the server is shutting down: retry as it is

  • 503 the database was closed (CLOSE DATABASE, a restore) while the request was running and is still there (DatabaseNotAvailableException, exceptionArgs CLOSED) (since v26.11.1, before it was 404), or 404 it was dropped (DatabaseIsClosedException)

  • 404 the database does not exist (Database not found), or 503 it is there and cannot serve the command now: closed, being restored (a write while a restore holds an open database is refused this way), dropped or recovered (see a database that is not available)

Example of insertion of a new Client by using parameters:

$ curl -X POST http://localhost:2480/api/v1/command/company \
       -d '{"language": "sql", "command": "create vertex Client set firstName = :firstName, lastName =  :lastName", params: {"firstName": "Jay", "lastName": "Miner"}}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Example of forcing an atomic transaction with autoCommit:

$ curl -X POST http://localhost:2480/api/v1/command/mydb \
       -d '{"language": "sql", "command": "INSERT INTO Account SET balance = 1000", "autoCommit": true}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Example of disabling automatic transaction wrapping:

$ curl -X POST http://localhost:2480/api/v1/command/mydb \
       -d '{"language": "sql", "command": "SELECT FROM Account LIMIT 1", "autoCommit": false}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Write statistics (stats)

When a command modifies data, the POST /api/v1/command/{database} response includes a stats object alongside result, reporting how many entities the command created, deleted or changed. The object is omitted for read-only queries (a command that changes nothing has no stats field). It is populated for OpenCypher writes — including counts aggregated across UNION branches and CALL { …​ } subqueries — and mirrors the counters the Bolt driver exposes through its result summary.

The shape is an ArcadeDB-native camelCase object with a stable set of keys (each counter is always present, 0 when unused):

Key Meaning

nodesCreated / nodesDeleted

Vertices created / deleted

relationshipsCreated / relationshipsDeleted

Edges created / deleted

propertiesSet

Properties assigned a value

labelsAdded / labelsRemoved

Type/label memberships added / removed

indexesAdded / indexesRemoved

Indexes created / dropped

constraintsAdded / constraintsRemoved

Constraints (UNIQUE, NOT_NULL, KEY, TYPED) added / removed

containsUpdates

true when any counter above is non-zero

Example — a Cypher write returns the counters it produced:

$ curl -X POST http://localhost:2480/api/v1/command/school \
       -d '{"language": "cypher", "command": "CREATE (a:Person {name:$n})-[:KNOWS]->(b:Person)", "params": {"n": "Ada"}}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password
{
  "user": "root",
  "result": [],
  "stats": {
    "nodesCreated": 2,
    "nodesDeleted": 0,
    "relationshipsCreated": 1,
    "relationshipsDeleted": 0,
    "propertiesSet": 1,
    "labelsAdded": 2,
    "labelsRemoved": 0,
    "indexesAdded": 0,
    "indexesRemoved": 0,
    "constraintsAdded": 0,
    "constraintsRemoved": 0,
    "containsUpdates": true
  }
}

The gRPC service carries the same counters in the write-result summary of ExecuteCommand; see gRPC API.

Numbers and arrays in params

Parameters keep the numbers as written: an integer beyond the long range, or a decimal with more than 17 significant digits, is read as a DECIMAL (BigDecimal) instead of being wrapped or rounded to a double. A number token with more than 1000 characters or an exponent beyond 1000 is refused with 400. A JSON array given as a parameter is a list, as when the statement runs embedded: written to a LIST property it is stored as that list, and [0.1, 3.141592653589793] keeps both doubles. (Since v26.10.1: before, such integers wrapped around and arrays with a fraction were narrowed to float32.)

Positional parameters

params given as a JSON array (or as an object keyed "0", "1", …​) binds its values by position. With SQL their number must match the placeholders of the statement: each ?, and each :name bound positionally, takes exactly one value (send null explicitly to bind a null), and in a sqlscript the placeholders of all its statements are counted together. A Postgres-style $N reads the value at position N and may leave lower positions unread. A request that does not match is refused with 400 before the statement runs (CommandSQLParsingException, the message names both counts). A list meant for one placeholder is one value, a nested array:

$ curl -X POST http://localhost:2480/api/v1/query/school \
       -d '{"language": "sql", "command": "select from Class where name in ?", "params": [["English", "Math"]]}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Named parameters (a JSON object with names as keys) are not counted. See also the Java API.

(since v26.11.1) Before, a surplus value was silently dropped ("params": ["English", "Math"] with where name in ? answered only the English rows) and a missing value was bound to null.

Typed parameter markers ($bytes, $int8)

JSON has no native byte[] type, so HTTP/JSON clients cannot send binary parameters with the standard scalar/array shapes. The query and command endpoints recognise two reserved single-key marker objects in any parameter value (named or positional) and decode them to a Java byte[] before the parameter binder sees them. The primary use case is sending an INT8-encoded vector to a LSM_VECTOR index without the float32 round trip — see Vector Encoding.

Marker Decodes to

{"$bytes": "<base64>"}

byte[] from a base64 string. Both standard (RFC 4648 section 4) and URL-safe (section 5, with - and _ in place of + and /) alphabets are accepted.

{"$int8": [v0, v1, …​]}

byte[] from an array of integers. Each element must round to an integer in [-128, 127]; fractional or out-of-range values are rejected with HTTP 400.

A single-key map whose key is one of these is decoded; multi-key maps that happen to contain a $bytes or $int8 key are passed through unchanged so user data with leading-$ keys is not silently transformed. The decoder also recurses into nested arrays and maps, so a list of typed markers (e.g. a batch of int8 vectors as a single parameter) works.

Examples:

# Base64-encoded byte array
curl -X POST http://localhost:2480/api/v1/query/mydb \
     -H "Content-Type: application/json" \
     --user root:arcadedb-password \
     -d '{
       "language": "sql",
       "command": "SELECT expand(`vector.neighbors`(\"Doc[embedding]\", :q, 5))",
       "params": {
         "q": {"$bytes": "AwIB/38="}
       }
     }'

# Integer array form (handy for debugging)
curl -X POST http://localhost:2480/api/v1/query/mydb \
     -H "Content-Type: application/json" \
     --user root:arcadedb-password \
     -d '{
       "language": "sql",
       "command": "SELECT expand(`vector.neighbors`(\"Doc[embedding]\", :q, 5))",
       "params": {
         "q": {"$int8": [1, 2, 3, -1, 127, -128]}
       }
     }'

Malformed markers (invalid base64, non-numeric $int8 element, value out of [-128, 127], null payload) return 400 Bad Request with a message naming the marker and the offending value.

(Since v26.10.1) Returns the nearest neighbors of a query vector in a dense LSM_VECTOR or sparse LSM_SPARSE_VECTOR index, without writing the vector.neighbors SQL by hand. ArcadeDB does not compute embeddings: the caller supplies the vector. The same request body and limits apply to the MCP vector_search tool and the gRPC VectorSearch RPC.

URL Syntax: /api/v1/vector/{database}/search

The JSON body accepts:

  • indexName (required) name of the vector index, for example Doc[embedding]

  • queryVector (required) array of numbers, not empty, finite values only. On a dense index its length must match the index dimensions and at least one value must be non-zero. With sparse: true it holds the weights matching queryIndices, or the full vector when queryIndices is omitted (zero entries are dropped)

  • k (optional, default 10) maximum number of results, between 1 and 1000

  • sparse (optional, default false) search an LSM_SPARSE_VECTOR index instead of a dense one. The index type must match the flag

  • queryIndices (optional, sparse only) dimension ids matching the queryVector weights: non-negative integers, no duplicates, same length as queryVector. Refused without sparse: true

  • efSearch (optional, dense only) HNSW search beam width, between 1 and 10000. Refused with sparse: true

  • filter (optional) read-only SQL WHERE predicate, at most 4096 characters, applied to each neighbor row (record properties are flattened, and @rid, @type, record plus distance or score are available). With a filter the search inspects a candidate window of k * 8 rows, at most 8000, reported as candidateLimit

The response carries:

  • indexName, sparse

  • scoring: the ranking direction, for example distance_lower_is_better:COSINE for a dense index, or score_higher_is_better:dot_product (score_higher_is_better:idf_weighted_dot_product with the IDF modifier) for a sparse one

  • candidateLimit: the candidate window inspected

  • truncated: true when k results were returned, so more matches may exist

  • count, results: each hit has rid, properties and either distance (dense, lower is better) or score (sparse, higher is better). Records deleted between the index scan and the load are skipped, so a search can return fewer than k hits

Responses:

  • 200 OK

  • 400 missing body, missing or out-of-range argument, unknown index or wrong index type (the message lists the available indexes), invalid filter expression

  • 401 missing or invalid credentials

  • 403 the user has no access to the database

  • 404 the database does not exist ({"error": "Database not found"}). A user without a grant on that name gets the 403 instead, so the answer does not reveal which databases exist

The route runs without a transaction. A 400 caused by a bad argument does not roll back a transaction the client opened with /begin.

Example:

$ curl -X POST http://localhost:2480/api/v1/vector/mydb/search \
       -d '{"indexName": "Doc[embedding]", "queryVector": [0.12, 0.85, 0.33], "k": 2, "filter": "lang = '\''en'\''"}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{
  "indexName": "Doc[embedding]",
  "sparse": false,
  "scoring": "distance_lower_is_better:COSINE",
  "candidateLimit": 16,
  "truncated": true,
  "count": 2,
  "results": [
    { "rid": "#12:4", "distance": 0.018, "properties": { "@rid": "#12:4", "@type": "Doc", "title": "Graphs 101", "lang": "en" } },
    { "rid": "#12:9", "distance": 0.041, "properties": { "@rid": "#12:9", "@type": "Doc", "title": "Vector search", "lang": "en" } }
  ]
}

Hybrid search (POST)

(Since v26.10.1) Fuses a vector leg, an optional full-text leg and an optional graph expansion leg into one ranked list, using the engine’s vector.fuse function. The same request body and limits apply to the MCP hybrid_search tool and the gRPC HybridSearch RPC.

URL Syntax: /api/v1/vector/{database}/hybrid

The JSON body accepts:

  • vectorIndexName (required) name of the vector index

  • queryVector, queryIndices, sparse, efSearch, filter (vector leg): same meaning and limits as in Vector search. The filter applies to the vector leg only

  • k (optional, default 10) maximum number of fused results, between 1 and 1000. Each retrieval leg fetches k * 4 rows, at most 1000, before fusion

  • fulltextIndexName and fulltextQuery (optional) add the full-text leg: a FULL_TEXT index and a Lucene-syntax query. They must be given together, otherwise the request is refused. Note the field is fulltextQuery, not queryText as in Full-text search

  • fusionStrategy (optional, default RRF) one of RRF, DBSF, LINEAR, case-insensitive

  • weights (optional) object with per-leg weights: vector (default 1.0), fulltext (default 1.0), expand (default 0.5). Each must be a finite number, not negative. Any other key, or a weight for a leg the request does not run, is refused

  • expand (optional) object that adds the graph expansion leg, seeded from the results of the retrieval legs (at most 256 seeds) and ranked by breadth-first discovery order:

    • edgeTypes (optional) edge type names to walk; each must exist. Empty or omitted walks every edge type

    • direction (optional, default out) one of out, in, both

    • maxDepth (optional, default 1) between 1 and 3

      The expansion returns at most 2000 records. Both the vector index and the full-text index must be declared on vertex types. Since the expansion leg has no score, it can only be used with RRF.

The response carries vectorIndexName, sparse, scoring, fused, truncated, count, results, plus:

  • fulltextIndexName when the full-text leg ran

  • fusionStrategy (upper-cased) when fused is true

  • legs: per-leg accounting. vector.count always; fulltext with indexName, similarity (BM25 or CLASSIC) and count when that leg ran; expand with direction, edgeTypes, maxDepth, truncated (fan-out cap reached), seedCount, seedsTruncated and count when expand was given

Fusion needs at least two legs that returned rows. When only the vector leg did, fused is false and each hit carries the native distance (dense) or score (sparse) instead of a fused score. Otherwise each hit has rid, fusedScore (higher is better), sources (the legs that produced it: vector, fulltext, expand) and properties, plus depth and path (RIDs from the seed, seed included) for a hit found by the expansion.

Responses:

  • 200 OK

  • 400 missing body, missing or out-of-range argument, unknown index, edge type or strategy, half a full-text leg, expand with a strategy other than RRF or on a non-vertex type, invalid filter or full-text query

  • 401 missing or invalid credentials

  • 403 the user has no access to the database

  • 404 the database does not exist ({"error": "Database not found"}). A user without a grant on that name gets the 403 instead, so the answer does not reveal which databases exist

Example:

$ curl -X POST http://localhost:2480/api/v1/vector/mydb/hybrid \
       -d '{
             "vectorIndexName": "Doc[embedding]",
             "queryVector": [0.12, 0.85, 0.33],
             "fulltextIndexName": "Doc[content]",
             "fulltextQuery": "+graph database",
             "k": 5,
             "expand": { "edgeTypes": ["Cites"], "direction": "out", "maxDepth": 1 }
           }' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return (abridged):

{
  "vectorIndexName": "Doc[embedding]",
  "sparse": false,
  "scoring": "distance_lower_is_better:COSINE",
  "legs": {
    "vector": { "count": 20 },
    "fulltext": { "indexName": "Doc[content]", "similarity": "BM25", "count": 7 },
    "expand": { "direction": "out", "edgeTypes": ["Cites"], "maxDepth": 1, "truncated": false,
                "seedCount": 24, "seedsTruncated": false, "count": 11 }
  },
  "fused": true,
  "fusionStrategy": "RRF",
  "fulltextIndexName": "Doc[content]",
  "truncated": true,
  "count": 5,
  "results": [
    { "rid": "#12:4", "fusedScore": 0.0325, "sources": ["vector", "fulltext"], "properties": { "@rid": "#12:4", "@type": "Doc", "title": "Graphs 101" } },
    { "rid": "#12:17", "fusedScore": 0.0081, "sources": ["expand"], "depth": 1, "path": ["#12:4", "#12:17"], "properties": { "@rid": "#12:17", "@type": "Doc", "title": "Property graphs" } }
  ]
}

Full-text search (POST)

(Since v26.10.1) Runs a Lucene-syntax query against a FULL_TEXT index and returns the matching documents, highest score first (ties ordered by RID). The same request body and limits apply to the MCP full_text_search tool and the gRPC FullTextSearch RPC.

URL Syntax: /api/v1/vector/{database}/fulltext

The JSON body accepts:

  • queryText (required) Lucene-syntax query, for example java or +java -python. Must not be blank

  • indexName (optional) the full-text index to search. Wins over typeName when both are given

  • typeName (optional) the type whose full-text index to search. Alone, it works only when the type has exactly one full-text index. An index declared on a supertype is named after the supertype

  • properties (optional) array of indexed property names, to pick one of several full-text indexes on typeName

  • limit (optional, default 10) maximum number of results, between 1 and 1000

One of indexName or typeName is required. The response carries indexName, similarity (BM25 or CLASSIC), count and results, where each hit has rid, score and properties. The limit is applied per bucket, so a record deleted between the index scan and the load is skipped rather than replaced, and a search can return fewer than limit hits.

Responses:

  • 200 OK

  • 400 missing body, blank queryText, limit out of range, no index addressed, unknown or ambiguous index (the message lists the available full-text indexes), invalid Lucene syntax

  • 401 missing or invalid credentials

  • 403 the user has no access to the database

  • 404 the database does not exist ({"error": "Database not found"}). A user without a grant on that name gets the 403 instead, so the answer does not reveal which databases exist

Example:

$ curl -X POST http://localhost:2480/api/v1/vector/mydb/fulltext \
       -d '{"typeName": "Doc", "queryText": "+graph -relational", "limit": 3}' \
       -H "Content-Type: application/json" \
       --user root:arcadedb-password

Return:

{
  "indexName": "Doc[content]",
  "similarity": "BM25",
  "count": 1,
  "results": [
    { "rid": "#12:4", "score": 2.71, "properties": { "@rid": "#12:4", "@type": "Doc", "title": "Graphs 101" } }
  ]
}

Begin a transaction (POST)

Begins a transaction on the server managed as a session. The response header contains the session id. Set this id in the following requests to execute them in the same transaction scope. See also /commit and /rollback.

URL Syntax: /api/v1/begin/{database}

Where:

  • database is the database name

The payload, optional as a JSON object, accepts the following parameters:

  • isolationLevel is the isolation level for the current transaction, either READ_COMMITTED (default) or REPEATABLE_READ.

A request without a body, or with a blank one, begins the transaction at the database default isolation level. A body that is not a valid JSON object (truncated, garbled, a scalar or a top-level array) is refused with 400 and no transaction is started: it is never taken as "no body", so a client that meant to ask for REPEATABLE_READ cannot silently get READ_COMMITTED.

Responses:

  • 204 OK

  • 400 invalid isolationLevel value, or a body that is not a valid JSON object

  • 403 invalid credentials

  • 409 transaction already started

  • 500 invalid database

Example:

$ curl -I -X POST http://localhost:2480/api/v1/begin/school \
       --user root:arcadedb-password

Returns the Session Id in the response header, example:

arcadedb-session-id: AS-ee056170-dc9b-4956-8d71-d7cfa01900d4

Use the session id in the request header of further commands you want to execute in the same transaction and execute /commit to commit the server side transaction or /rollback to rollback the changes. After a period of inactivity (default is 30 seconds), the server automatically rollback and purge expired transactions.

Commit a transaction (POST)

Commits a transaction on the server. Set the session id obtained with the /begin command as a header of the request. See also /begin and /rollback.

URL Syntax: /api/v1/commit/{database}

Where:

  • database is the database name

Set the session id returned from the /begin command in the request header. If the session (and therefore the server side transaction) is unknown, expired or already ended, 404 is returned: nothing was committed.

The commit ends the session, whether it succeeds or fails: a commit that fails rolls the transaction back, and the answer carries the header arcadedb-session-closed: true, so there is no need to send a /rollback afterwards. (Since v26.10.1: before, the session of a failed commit stayed registered until it timed out.)

A command that fails inside a session rolls the session’s transaction back. When the transaction held changes, they are lost, so the session is marked as rolled back: the next command naming it is refused with 404, and /commit answers 404 with a message saying that the transaction was rolled back, instead of 204. Send a /rollback (or the /commit) to end it. When nothing was lost (a failed read, an error before the first write) the session goes on in a fresh transaction. (Since v26.10.1: before, the commands after the failure ran in autocommit and /commit answered 204.)

A /commit retried while the first one is still running (a client that timed out and sent it again) waits up to 5 seconds for the first one to finish, then answers 204 if that commit succeeded and 404 if it failed. A retry that reaches another server, or arrives after a restart, cannot know the outcome and answers 404: send an X-Request-Id to make a retry safe in every case. (Since v26.11.1: before, the retry answered 404 even when the first commit succeeded.)

Response:

  • 204 OK

  • 401 invalid credentials

  • 404 the session is unknown, expired or already ended, or its transaction was rolled back by a failed command

Example:

$ curl -I -X POST http://localhost:2480/api/v1/commit/school \
       -H "arcadedb-session-id: AS-ee056170-dc9b-4956-8d71-d7cfa01900d4" \
       --user root:arcadedb-password

Rollback a transaction (POST)

Rollbacks a transaction on the server. Set the session id obtained with the /begin command as a header of the request. See also /begin and /commit.

URL Syntax: /api/v1/rollback/{database}

Where:

  • database is the database name

Set the session id returned from the /begin command in the request header. If the session (and therefore the server side transaction) is expired, then an internal server error is returned.

The commit ends the session, whether it succeeds or fails: a commit that fails rolls the transaction back, and the answer carries the header arcadedb-session-closed: true, so there is no need to send a /rollback afterwards. (Since v26.10.1: before, the session of a failed commit stayed registered until it timed out.)

Response:

  • 204 OK

  • 403 invalid credentials

  • 500 transaction expired, not found, not begun

Example:

$ curl -I -X POST http://localhost:2480/api/v1/rollback/school \
       -H "arcadedb-session-id: AS-ee056170-dc9b-4956-8d71-d7cfa01900d4" \
       --user root:arcadedb-password

Batch import vertices and edges (POST)

High-performance bulk import of vertices and edges using the GraphBatch engine under the hood. Supports JSONL and CSV input formats with streaming parsing — the server never loads the entire request body into memory, so you can push millions of records in a single request.

URL Syntax: /api/v1/batch/{database}

Where:

  • database is the database name

The Content-Type header determines the input format:

  • application/x-ndjson or application/jsonl — JSONL (newline-delimited JSON)

  • text/csv — CSV

All vertices must appear before edges in the request body. Edges reference vertices either by temporary ID or by existing database RID.

JSONL Format

Each line is a JSON object with the following meta fields:

Field Required Description

@type

Yes

"vertex" (or "v") for vertices, "edge" (or "e") for edges

@class

Yes

The vertex or edge type name (must exist in schema)

@id

No

Client-assigned temporary ID for vertices. Used by edges to reference newly created vertices.

@from

Edges only

Source vertex reference: a temporary ID (e.g., "t1") or an existing RID (e.g., "#12:0")

@to

Edges only

Destination vertex reference: a temporary ID or an existing RID

All other fields are treated as properties.

A control key sent on the kind of line that cannot use it is refused with 400, naming the line: @id on an edge, @from or @to on a vertex. Before 26.10.1 such a key was silently dropped — neither stored nor reported — so a client that models both line shapes with one struct got a load that looked clean while the value was gone. A key carrying nothing is still accepted, so JSON null and the empty string are ignored.

Example:

{"@type":"vertex","@class":"Person","@id":"t1","name":"Alice","age":30}
{"@type":"vertex","@class":"Person","@id":"t2","name":"Bob","age":25}
{"@type":"edge","@class":"KNOWS","@from":"t1","@to":"t2","since":2020}
{"@type":"edge","@class":"KNOWS","@from":"#12:0","@to":"t2","weight":0.8}

CSV Format

The first line is the column header. A --- sentinel row separates the vertex section from the edge section, followed by a new header row for edges.

Example:

@type,@class,@id,name,age
vertex,Person,t1,Alice,30
vertex,Person,t2,Bob,25
---
@type,@class,@from,@to,since
edge,KNOWS,t1,t2,2020

CSV parsing supports RFC 4180 quoting (double-quote escaping). Numeric values are auto-detected as integers or floats. Boolean values (true/false) are also auto-detected.

The same rule as JSONL applies to a control value, not to the column: a row carrying a value in a control column its kind cannot use — @id on an edge row, @from/@to on a vertex row — is refused with 400, while an empty one is ignored. That keeps a single header naming all five control columns across both sections working, as long as the inapplicable columns are left empty on each row.

Query Parameters

All parameters are optional and map directly to GraphBatch builder options:

Parameter Default Description

batchSize

100000

Maximum edges buffered before auto-flush

lightEdges

false

If true, property-less edges are stored as connectivity only (saves ~33% I/O)

wal

false

Enable Write-Ahead Logging for crash safety

parallelFlush

true

Parallelize edge connection across async threads

preAllocateEdgeChunks

true

Pre-allocate edge segments on vertex creation

edgeListInitialSize

2048

Initial segment size in bytes (64-8192)

bidirectional

true

Connect the incoming side of the edges whose type is declared bidirectional; a UNIDIRECTIONAL type never gets it. Set to false only to load unidirectional edge types: an edge of a bidirectional type is then refused with HTTP 400 (since 26.10.1)

commitEvery

50000

Edges per sub-transaction within a flush

expectedEdgeCount

0

Hint for auto-tuning batch size

Response

The response is a JSON object with import statistics:

{
  "verticesCreated": 2,
  "edgesCreated": 1,
  "elapsedMs": 42,
  "idMapping": {
    "t1": "#9:0",
    "t2": "#9:1"
  }
}

The idMapping field is included only when temporary IDs (@id) were used. It maps each temporary ID to the actual RID assigned by the database.

Because this response is built in full before anything is sent, the mapping of a very large load is dropped rather than risk exhausting memory: past 10,000 entries the server replies with idMappingOmitted: true and idMappingSize instead. Send idMapping=true to demand it anyway, or use the streaming response below, which has no such limit.

Streaming response

Send Accept: application/x-ndjson to be answered as the load runs instead of once at the end. The response is one JSON object per line:

{"progress":{"phase":"vertices","verticesCreated":10000,"edgesCreated":0,"idMapping":{"t1":"#9:0","...":"..."}}}
{"progress":{"phase":"vertices","verticesCreated":20000,"edgesCreated":0,"idMapping":{"...":"..."}}}
{"summary":{"verticesCreated":20000,"edgesCreated":0,"elapsedMs":1234,"idMappingStreamed":true,"idMappingSize":20000}}

Each progress line acknowledges a committed chunk and carries the temporary-ID mapping for that chunk only. Concatenate them to obtain the whole mapping, and check the total against idMappingSize on the last line. Because neither side ever holds more than one chunk, the 10,000-entry limit above does not apply here: a load of any size gets its complete mapping.

The stream always ends with either a summary line (success) or an error line (failure); a stream that ends with neither did not arrive complete. On failure the error line carries the same counters the buffered response would have (verticesCreated, edgesCreated, partialCommit), plus status, the status code it would have been sent under, since the status line can no longer be changed once the stream has started, and the members of the buffered error body (error, exception, exceptionArgs, requestId, and detail outside production mode). (Since v26.10.2) a retryable refusal also carries retryAfter, the integer seconds the buffered response would have sent as Retry-After (a 503, or a 409 for a request still in flight).

Sending no Accept header, or any other value, returns the single buffered object described above, unchanged.

The Java driver’s RemoteGraphBatch uses this encoding automatically, so a bulk load through it is not limited by the mapping size either. Call withProgressListener(…​) on the builder to be notified of each chunk.

Responses:

  • 200 OK

  • 400 invalid input format, missing required fields, unknown temporary ID, vertices after edges

  • 403 invalid credentials

  • 413 the body exceeds server.httpBodyContentMaxSize; the load is cut short and the records loaded before the cut stay loaded

  • 503 / 504 the request could not be forwarded (for example from a follower to the leader); the records loaded before the failure stay loaded

A 413, 503 or 504 does NOT mean that nothing was applied. A batch that is cut short may be partially applied: the server log carries the counts, but the HTTP answer does not. A client that reads such an error as "rejected, nothing applied" and resends the whole payload after raising the limit or splitting it creates duplicates, unless the payload is idempotent (for example @id-keyed upserts). Deduplicate on the client side, or reload into a clean target database.
This endpoint is NOT atomic by design. The underlying GraphBatch commits internally in chunks for maximum throughput. The response tells you exactly how many records were committed. Treat it as a bulk-loading operation, not a transactional one. If the server dies during the load, the edges already committed are not traversable until CHECK DATABASE FIX relinks them (since v26.11.1), see GraphBatch.
For maximum throughput, group vertices by type in the input. The endpoint batches consecutive same-type vertices into a single createVertices() call. Interleaving types forces smaller, less efficient batches.

Examples

JSONL import with light edges:

$ curl -X POST "http://localhost:2480/api/v1/batch/mydb?lightEdges=true" \
       -H "Content-Type: application/x-ndjson" \
       --data-binary @graph-data.jsonl \
       --user root:arcadedb-password

CSV import:

$ curl -X POST http://localhost:2480/api/v1/batch/mydb \
       -H "Content-Type: text/csv" \
       --data-binary @graph-data.csv \
       --user root:arcadedb-password

Inline JSONL with WAL enabled:

$ curl -X POST "http://localhost:2480/api/v1/batch/mydb?wal=true" \
       -H "Content-Type: application/x-ndjson" \
       --user root:arcadedb-password \
       -d '{"@type":"vertex","@class":"Person","@id":"p1","name":"Alice"}
{"@type":"vertex","@class":"Person","@id":"p2","name":"Bob"}
{"@type":"edge","@class":"KNOWS","@from":"p1","@to":"p2"}'

Python example:

import requests

data = (
    '{"@type":"vertex","@class":"Person","@id":"p1","name":"Alice"}\n'
    '{"@type":"vertex","@class":"Person","@id":"p2","name":"Bob"}\n'
    '{"@type":"edge","@class":"KNOWS","@from":"p1","@to":"p2"}\n'
)

resp = requests.post(
    "http://localhost:2480/api/v1/batch/mydb?lightEdges=true",
    auth=("root", "arcadedb-password"),
    headers={"Content-Type": "application/x-ndjson"},
    data=data,
)
print(resp.json())
# {'verticesCreated': 2, 'edgesCreated': 1, 'elapsedMs': 15, 'idMapping': {'p1': '#9:0', 'p2': '#9:1'}}

JavaScript (Node.js) example:

const resp = await fetch("http://localhost:2480/api/v1/batch/mydb", {
  method: "POST",
  headers: {
    "Content-Type": "application/x-ndjson",
    "Authorization": "Basic " + btoa("root:arcadedb-password"),
  },
  body: [
    '{"@type":"vertex","@class":"Person","@id":"p1","name":"Alice"}',
    '{"@type":"vertex","@class":"Person","@id":"p2","name":"Bob"}',
    '{"@type":"edge","@class":"KNOWS","@from":"p1","@to":"p2"}',
  ].join("\n"),
});
console.log(await resp.json());