Docs
View as markdown

HTTP Query API

The routes, request and response JSON, status codes, error bodies, transactions, content types, limits, and authentication of relate's HTTP Query API.

http

relate serve --http PORT hosts the HTTP Query API The JSON-over-HTTP statement endpoint relate serve --http <port> hosts, in the shape of Neo4j's Query API v2: POST /db/<name>/query/v2. Read more in the glossary: one JSON statement One unit of Cypher that relate parses, plans and runs, such as a MATCH … RETURN read or a CREATE write; a script holds several, separated by ;. Read more in the glossary per POST to /db/<name>/query/v2, one JSON envelope back, in the request and response shape of Neo4j's Query API v2. It also serves managed transactions under /query/v2/tx and a public discovery document at GET /. No OpenAPI document is published for it; the tables on this page are the contract, and every response shown was captured from relate 0.1.2.

Synopsis

relate serve --database PATH --http PORT [--host HOST] [--auth USER:PASSWORD] [--origin ORIGIN] [--tx-idle-timeout-secs N] [--max-open-transactions N] [--read-only[=FRESHNESS]]
ItemValue
TransportHTTP/1.1 only, plaintext, one request per connection (connection: close on every response)
Bind127.0.0.1:PORT; --host changes the address, with no TLS on any address
Statement routePOST /db/<name>/query/v2: one auto-commit statement
Transaction routesPOST /db/<name>/query/v2/tx and the routes under it
<name>A namespace: neo4j, system or system_ontologies
AuthenticationHTTP Basic on every route but GET / and OPTIONS, from one credential shared with the other relate serve surfaces
Request typesapplication/json (plain parameters) and application/vnd.neo4j.query.v1.2 (typed parameters)
Response typesapplication/json (plain values) or application/vnd.neo4j.query (typed values), chosen by Accept
FlagsOn relate serve and in CLI flags

Routes

MethodPathAnswer
GET/200 and the discovery document; no credential needed
POST/db/<name>/query/v2200 and the result of one auto-commit statement
POST/db/<name>/query/v2/tx201: begins a transaction, runs the body's statement when there is one, and returns the result with a transaction block
POST/db/<name>/query/v2/tx/<id>200: runs a statement in the open transaction; an empty body keeps the transaction alive
POST/db/<name>/query/v2/tx/<id>/commit200: runs the body's statement when there is one, then commits and closes the transaction
DELETE/db/<name>/query/v2/tx/<id>202 and {}: rolls back and closes the transaction
OPTIONSAny path204: a CORS preflight, with no credential needed (Browser requests)
Another methodA known route405, method not allowed
AnyAny other path404, not found

The discovery document names the routes. With --bolt in the same process, it also carries bolt_direct. This server opens the store read-only:

relate serve --database ./team --http 8851 --bolt 8852 --read-only
curl -s http://127.0.0.1:8851/
{"query":"http://127.0.0.1:8851/db/{databaseName}/query/v2","transaction":"http://127.0.0.1:8851/db/{databaseName}/query/v2/tx","neo4j_version":"0.1.2","neo4j_edition":"community","auth_config":{"oidc_providers":[]},"bolt_direct":"bolt://127.0.0.1:8852"}
KeyValue
queryThe statement route, built from the request's Host header
transactionThe transaction route, the same URL plus /tx
neo4j_versionThe relate version
neo4j_editioncommunity
auth_config{"oidc_providers": []}: there is no single sign-on
bolt_directThe Bolt URL, only when the process also serves --bolt

Request

The body of a statement or transaction request is one JSON object:

KeyTypeDefaultMeaning
statementstringrequired on the statement route; optional on the transaction routesOne Cypher statement. A second statement after ; is a syntax error
parametersobjectnoneThe statement's parameters, each bound to $name. A statement that names a parameter the object lacks fails with 42N02; nothing is bound as null
includeCountersbooleanfalsetrue puts counters on a response that wrote nothing
accessModestringnone"READ" on the begin route refuses writes for the rest of that transaction; the run and commit routes and the auto-commit route ignore it

Other keys are ignored. On the statement route, a body that is not JSON, or that has no statement, is a 400 with code 42N06. On the transaction routes, a body without statement, an empty body included, runs nothing: it opens a transaction on the begin route and keeps one alive on the run route.

The request's Content-Type selects how parameters are read:

Request Content-TypeParameters
application/jsonPlain JSON values. An object shaped like {"$type": …, "_value": …} stays a map
application/vnd.neo4j.query.v1.2Plain JSON values, or typed values {"$type": <type>, "_value": <value>} as Neo4j's clients send them. A temporal value arrives as its ISO 8601 string (date($p) builds the typed value in Cypher), a point as its {coordinates, crs} map, a vector as its number list. An unknown $type is a 400, unknown typed parameter $type "<type>"
None, text/plain, or a form type415, before the credential check
Any other, application/vnd.neo4j.query included400, content-type "<type>" is not a statement request type: application/json for plain parameters, or application/vnd.neo4j.query.v1.2 for typed envelope parameters

Response

The Accept header picks the response type and the value encoding; the first entry the server can produce wins:

AcceptResponse Content-TypeValues
application/json, */*, or no headerapplication/jsonPlain JSON
application/vnd.neo4j.query, or a versioned form such as application/vnd.neo4j.query.v1.1+jsonapplication/vnd.neo4j.queryTyped: every value {"$type": …, "_value": …}
Only types the server cannot produceapplication/json406, not acceptable: this server responds application/json or application/vnd.neo4j.query

Error responses are always application/json.

Success envelope

KeyPresentValue
data.fieldsAlwaysThe column names
data.valuesAlwaysOne array per row, aligned with fields
countersWhen the statement wrote, or includeCounters is trueThe 11 mutation counters, zeros included: constraints_added, constraints_removed, indexes_added, indexes_removed, labels_added, labels_removed, nodes_created, nodes_deleted, properties_set, relationships_created, relationships_deleted
bookmarksAlways[]
queryPlanOn an EXPLAIN statementThe plan tree (Plans and notifications)
profiledQueryPlanOn a PROFILE statementThe plan tree with its measured metrics
notificationsOn an EXPLAIN or PROFILE statement that raised anyThe plan's notifications
timingsWhen a statement ranThe statement's phase timings in milliseconds: elapsed_ms, execute_ms, lex_parse_ms, lower_ms, parse_ms, plan_ms, result_generation_ms and rewrite_ms
planCacheWhen a statement ranThe plan cache's outcome, such as miss, or hit-raw-fastpath for a statement the server planned before
transactionOn a begin or run in a managed transactionid and expires (Transactions)

timings and planCache follow the members of Neo4j's Query API v2 shape, and their values differ on every request. The envelope carries no column types; the MCP server and the CLI's --mode envelope carry them.

The examples on this page run against the shared example graph of three people and two projects, served writable because some of them write:

relate serve --database ./team --http 8851 --auth <user>:<password>

A read with a parameter returns rows and no counters:

curl -s -u <user>:<password> -X POST http://127.0.0.1:8851/db/neo4j/query/v2 \
  -H 'Content-Type: application/json' \
  -d '{"statement": "MATCH (p:Person {role: $role}) RETURN p.name AS name, p ORDER BY name", "parameters": {"role": "engineer"}}'
{"data":{"fields":["name","p"],"values":[["Ada",{"elementId":"0","labels":["Person"],"properties":{"name":"Ada","role":"engineer"}}],["Grace",{"elementId":"1","labels":["Person"],"properties":{"name":"Grace","role":"engineer"}}]]},"bookmarks":[],"timings":{"elapsed_ms":0.495042,"execute_ms":0.288666,"lex_parse_ms":0.034875,"lower_ms":0.055083,"parse_ms":0.132209,"plan_ms":0.063459,"result_generation_ms":0.00025,"rewrite_ms":0.007708},"planCache":"miss"}

A write returns counters:

curl -s -u <user>:<password> -X POST http://127.0.0.1:8851/db/neo4j/query/v2 \
  -H 'Content-Type: application/json' \
  -d '{"statement": "CREATE (:Project {name: $name, language: $language})", "parameters": {"name": "docs", "language": "Markdown"}}'
{"data":{"fields":[],"values":[]},"counters":{"constraints_added":0,"constraints_removed":0,"indexes_added":0,"indexes_removed":0,"labels_added":1,"labels_removed":0,"nodes_created":1,"nodes_deleted":0,"properties_set":2,"relationships_created":0,"relationships_deleted":0},"bookmarks":[],"timings":{"elapsed_ms":1.1645830000000001,"execute_ms":1.1110829999999998,"lex_parse_ms":0.002375,"lower_ms":0.003292,"parse_ms":0.050459,"plan_ms":0.001375,"result_generation_ms":0.000166,"rewrite_ms":0.00020800000000000001},"planCache":"miss"}

Value encoding

Cypher typePlain (application/json)Typed (application/vnd.neo4j.query)
Integer, floatA JSON number{"$type": "Integer", "_value": "2"}, {"$type": "Float", "_value": "1.5"}: the number as a string
String, boolean, nullThe JSON valueString, Boolean, Null with the JSON value
List, mapA JSON array or objectList or Map, with each member typed
Node{"elementId", "labels", "properties"}Node with _element_id, _labels and typed _properties
Relationship{"elementId", "startNodeElementId", "endNodeElementId", "type", "properties"}Relationship with the same fields in the typed form
PathA JSON array alternating nodes and relationshipsPath
Date, time, datetime, durationThe ISO 8601 string: "2024-01-02", "2024-01-02T03:04:05Z", "2024-01-02T03:04:05+01:00[Europe/Stockholm]", "P1DT2H"Date, Time, LocalTime, LocalDateTime, OffsetDateTime, ZonedDateTime or Duration, with the string
Point{"type": "Point", "coordinates": [13.0, 55.6], "crs": {…}}, crs naming the SRID, such as 4326 (wgs-84)Point with coordinates and crs
Byte arrayA list of integers from 0 to 255A List of Integer

The elementId in this API is the node's or relationship's number as a string ("0"); Bolt 5.4 carries the elementId() form, such as 4:relate-neo4j:n0. A byte array reaches the engine only as a Bolt or Rust parameter, never through this API's JSON.

Plans and notifications

An EXPLAIN statement returns no rows (data.fields and data.values are empty) and its plan under queryPlan. A PROFILE statement returns its own rows and its plan under profiledQueryPlan. A statement without either prefix carries neither key and no notifications.

Plan node keyValue
operatorTypeThe operator with an @relate suffix, such as ProduceResults@relate
argumentsId, Details and the operator's other arguments; on the root, string-representation (the plan as text), version, planner, planner-impl, planner-version, runtime, runtime-impl and runtime-version; on a PROFILE plan, Rows, DbHits, TimeNs, MemoryBytes and the other measured values
identifiersA list of strings
childrenThe child nodes; [] on a leaf
dbHits, records, timeThe measured values; 0 on an EXPLAIN plan
hasPageCacheStats, pageCacheHits, pageCacheMisses, pageCacheHitRatioAlways false, 0, 0 and 0.0
warningsOn a node with a warning: {"name": …} entries, such as {"name": "CartesianProduct"}

Each notification has code, title, description, severity and category. The remedy is appended to description as (remedy: <remedy>); there is no position, gql_status or remedy key:

{"category":"PERFORMANCE","code":"Neo.ClientNotification.Statement.CartesianProduct","description":"the MATCH patterns share no join anchor (remedy: Give the patterns a join anchor: reuse a node variable across them, or add a pattern that relates their endpoints; otherwise run them as separate queries.)","severity":"INFORMATION","title":"Cartesian product"}

Transactions

A managed transaction A unit of work that commits as a whole or not at all; each statement is its own transaction unless an explicit begin … commit groups several. Read more in the glossary is an explicit engine transaction that spans several requests: commit makes its writes durable, rollback discards them, and the auto-commit route does not see them until commit. Every reply about an open transaction carries a transaction block:

KeyValue
idtx-<n>, counting from tx-0 in each server process, never reused
expiresWhen the transaction expires if nothing touches it, an RFC 3339 UTC time with milliseconds, such as 2026-11-08T09:53:54.828Z; absent when expiry is off
RuleBehavior
Idle expiryA transaction untouched for --tx-idle-timeout-secs seconds (default 60, RELATE_TX_IDLE_TIMEOUT_SECS, 0 turns expiry off) is rolled back. A begin or a run, the empty keep-alive body included, resets the clock, even when its statement failed
ExpiredThe next request to that id answers 404, 42N90, transaction <id> expired after <n>s idle, when that request is the one that finds it expired. Once another request has swept it, the id answers 404, 42000, transaction <id> not found
Open-transaction capAt most --max-open-transactions open at once (default 128, RELATE_MAX_OPEN_TRANSACTIONS, 0 is unlimited). A begin over the cap answers 429, 42N91, transaction registry full: <n> open transactions (close one or raise --max-open-transactions)
Closed or unknown id404, 42000, transaction <id> not found; also while another request's statement in the same transaction is still running
Failed statementThe transaction stays open but has failed, and the error response carries the transaction block. Every later statement and the commit answer 400, 42000, semantic error: transaction failed; rollback required (errors reference); an empty keep-alive body still answers 200. A commit that gets this answer closes the transaction with nothing written, and DELETE closes it. A refusal under accessMode: "READ" does not fail the transaction
Failed begin statementThe new transaction is rolled back
accessMode: "READ"A write or schema statement in that transaction answers 400, 42000, writing is not allowed in a READ transaction, and the transaction stays open
Store opened --read-onlyA write inside a transaction runs and is visible to that transaction; the commit answers 400, 42N18, and nothing is written
Server stopsThe writes of every open transaction are discarded

A transaction from begin to commit, with the store's people counted from inside it and from the auto-commit route:

curl -s -u <user>:<password> -X POST http://127.0.0.1:8851/db/neo4j/query/v2/tx \
  -H 'Content-Type: application/json' \
  -d '{"statement": "CREATE (:Person {name: $name}) RETURN $name AS name", "parameters": {"name": "Eve"}}'
curl -s -u <user>:<password> -X POST http://127.0.0.1:8851/db/neo4j/query/v2/tx/tx-0 \
  -H 'Content-Type: application/json' \
  -d '{"statement": "MATCH (p:Person) RETURN count(p) AS people"}'
curl -s -u <user>:<password> -X POST http://127.0.0.1:8851/db/neo4j/query/v2 \
  -H 'Content-Type: application/json' \
  -d '{"statement": "MATCH (p:Person) RETURN count(p) AS people"}'
curl -s -u <user>:<password> -X POST http://127.0.0.1:8851/db/neo4j/query/v2/tx/tx-0/commit \
  -H 'Content-Type: application/json' \
  -d '{"statement": "MATCH (p:Person) RETURN count(p) AS people"}'

The four responses, in order; the expires and timings values differ on every run:

{"data":{"fields":["name"],"values":[["Eve"]]},"counters":{"constraints_added":0,"constraints_removed":0,"indexes_added":0,"indexes_removed":0,"labels_added":1,"labels_removed":0,"nodes_created":1,"nodes_deleted":0,"properties_set":1,"relationships_created":0,"relationships_deleted":0},"bookmarks":[],"timings":{"elapsed_ms":0.41541700000000004,"execute_ms":0.356708,"lex_parse_ms":0.004416,"lower_ms":0.002875,"parse_ms":0.054333000000000006,"plan_ms":0.0022919999999999998,"result_generation_ms":0.00020800000000000001,"rewrite_ms":0.00025},"planCache":"miss","transaction":{"expires":"2026-11-10T01:57:39.290Z","id":"tx-0"}}
{"data":{"fields":["people"],"values":[[4]]},"bookmarks":[],"timings":{"elapsed_ms":0.132084,"execute_ms":0.056,"lex_parse_ms":0.010875,"lower_ms":0.019791,"parse_ms":0.041708,"plan_ms":0.028292,"result_generation_ms":0.000292,"rewrite_ms":0.00175},"planCache":"miss","transaction":{"expires":"2026-11-10T01:57:39.303Z","id":"tx-0"}}
{"data":{"fields":["people"],"values":[[3]]},"bookmarks":[],"timings":{"elapsed_ms":0.387084,"execute_ms":0.33725,"lex_parse_ms":0.0,"lower_ms":0.0,"parse_ms":0.000334,"plan_ms":0.047875,"result_generation_ms":0.00025,"rewrite_ms":0.0},"planCache":"hit-raw-fastpath"}
{"data":{"fields":["people"],"values":[[4]]},"bookmarks":[],"timings":{"elapsed_ms":0.066708,"execute_ms":0.037958,"lex_parse_ms":0.0,"lower_ms":0.0,"parse_ms":0.000209,"plan_ms":0.027416999999999997,"result_generation_ms":0.00020800000000000001,"rewrite_ms":0.0},"planCache":"hit-raw-fastpath"}

Inside the transaction there are four people; the auto-commit route still counts three until the commit.

Bookmarks

bookmarks is [] on every response, writes included, and a request's own bookmarks key is ignored. Every committed transaction is visible to the next request on any connection, because one process serves one store. The Bolt surface returns a bookmark for each commit (Bolt bookmarks).

Status codes and errors

An error response is application/json with one key, errors, holding one object with code and message:

{"errors":[{"code":"42N00","message":"semantic error: DatabaseNotFound: Graph not found: movies"}]}
StatuscodeWhenExample message
40042N06On the statement route, the body is not JSON or has no statement; a Cypher syntax errorinvalid request body: missing field `statement` at line 1 column 19
40042N02The statement names a parameter the request does not bindsemantic error: ParameterMissing: Expected parameter(s): x
40022G03A type error, division by zero includedtype error: / by zero
40051N27A database administration commandsemantic error: UnsupportedAdministrationCommand: Unsupported administration command: CREATE DATABASE foo
40042N18A write against a store opened --read-onlysemantic error: WriteOnReadOnlyAccessDatabase: No write operations are allowed on this database. The database is in read-only mode on this Neo4j instance.
40042000Any other refused statement; a statement or commit in a transaction that has already failed; an unknown typed parameter; an unsupported request Content-Type; a write in a READ transaction; a body over the cap; a request line that is not HTTP/1.1unsupported HTTP version HTTP/1.0: this surface answers HTTP/1.1
40142NFFNo credential, or a wrong one, after a 1-second delay; with WWW-Authenticate: Basic realm="relate"authentication required
40342000A browser Origin outside the allowlistorigin is not allowed
40442N00An unknown namespace, or any unknown pathnot found
40442000A transaction id that is closed, unknown, or busytransaction tx-1 not found
40442N90A transaction that expired idletransaction tx-2 expired after 2s idle
40542000A method the route does not takemethod not allowed
40642000An Accept the server cannot satisfynot acceptable: this server responds application/json or application/vnd.neo4j.query
41542000A POST with no Content-Type, text/plain or a form typethe request content type is browser-"simple" — a cross-origin form cannot send a non-simple type without a preflight
42942N91A begin over the open-transaction captransaction registry full: 2 open transactions (close one or raise --max-open-transactions)
50050N42An engine-internal failure; the message is generic
50042000A storage, IO or lock failure; the message is generic

Each code's cause and remedy is in the HTTP Query API errors.

Authentication

Every route except GET / and OPTIONS asks for HTTP Basic authentication (RFC 7617). The check runs before routing, so an unknown path without a credential is a 401, not a 404, and it runs before the body is read.

SourceCredential
--auth USER:PASSWORDThat user and password. A value with no colon exits 1 with Error: --auth: expected user:password or 'none'
RELATE_AUTHThe same forms, read when --auth is absent
--auth noneNo credential is checked
NeitherA generated token for the user relate, printed on stderr at startup; saved as <store>.auth-token (mode 0600) beside a directory or .rl store and reused on restart; new on every start for :memory:

The credential is the one relate serve shares with its --mcp and --bolt listeners. There are no users, roles or per-namespace permissions. Every surface is plaintext, so a credential sent to a non-loopback --host crosses the network unencrypted; Security lists the defaults.

Browser requests

A POST or DELETE that carries an Origin header must name an allowed origin, or it is refused with 403 before the credential check. A request without Origin, such as one from curl, is not checked.

SettingAllowed origins
No --originLoopback (http or https on localhost, 127.0.0.1 or [::1], any port), https://browser.neo4j.io and https://console.neo4j.io
--origin <url>, repeatableOnly the origins named; loopback is no longer implied
--origin '*'Any origin
ResponseHeader
Any response to an allowed OriginAccess-Control-Allow-Origin with that origin; none for an origin outside the list
OPTIONS preflight204 with Access-Control-Allow-Methods: GET, POST, OPTIONS and Access-Control-Allow-Headers: Authorization, Content-Type, sentry-trace, baggage

A value that is not an origin URL or * stops the server before it starts:

relate serve --database :memory: --http 7474 --read-only --origin example.com
echo "exit=$?"
Error: serve --origin expects an origin URL (http(s)://…) or *, got "example.com"
Usage: relate serve --database PATH [--host HOST] [--http PORT] [--origin ORIGIN] [--mcp PORT|stdio] [--bolt PORT [--bolt-identity STRING] [--ws-allow-origin ORIGIN] [--ws-max-connections N]] [--read-only[=FRESHNESS]] [--load SOURCE] [--auth USER:PASSWORD] [--concurrent-logical-workspace-limit SIZE] [--max-traversal-depth N] [--expression-memory-limit SIZE] [--safe | --no-safe] [--logical-graph-size-limit SIZE] [--durability-mode MODE] [--snapshot-format F]
Run 'relate serve --help' for more information.
exit=2

Compression and framing

ConditionResponse
Accept-Encoding accepts gzip (or x-gzip), and the body is at least 1024 bytes and smaller compressedContent-Encoding: gzip; no other coding is produced
Any response on a query routeVary: Accept-Encoding; the discovery document and the preflight carry none
An uncompressed body of 65536 bytes or more, measured before gzipTransfer-Encoding: chunked instead of Content-Length; the chunk boundaries carry no meaning
Any other bodyContent-Length

RETURN range(1, 500) AS r with Accept-Encoding: gzip came back gzipped in 1091 bytes, RETURN 1 AS i uncompressed, and RETURN range(1, 20000) AS r (109198 bytes uncompressed) chunked, even gzipped to 41038 bytes. These sizes move by a few bytes from run to run with the timings values. Clients that decode gzip and chunked framing, curl --compressed and the Neo4j drivers among them, see the same JSON either way.

Limits

Every row is a limit.

LimitValueOver the limit
Request body16 MiB400, 42000, malformed request: body too large
Statements per request1A syntax error
Requests per connection1The server closes the connection after each response
Header deadline10 seconds for the whole header block (--header-deadline-secs, RELATE_HEADER_DEADLINE_SECS; 0 turns it off; at most 86400)The connection is dropped with no reply
Body deadline60 seconds for the whole body (--body-deadline-secs, RELATE_BODY_DEADLINE_SECS; 0 turns it off; at most 86400)The connection is dropped with no reply
Concurrent connections128, shared with the --mcp and --bolt listeners (--max-connections, RELATE_MAX_CONNECTIONS)The connection is accepted, then closed
Idle managed transaction60 seconds (--tx-idle-timeout-secs)Rolled back; 404, 42N90
Open managed transactions128 (--max-open-transactions)429, 42N91
Failed credential checksOne answer per second per connectionThe 401 waits 1 second
Traversal depthAn explicit hop bound up to 500 (--max-traversal-depth, 1 to 2000)400 with the TraversalDepthExceeded message

There is no per-statement timeout and no row cap: a response holds every row of its result. With both transaction limits set to 0 on a non-loopback --host, the server prints a startup warning that an abandoned transaction would never expire.

See also