Serve the HTTP Query API
Run relate serve --http and send Cypher as JSON over HTTP in the shape of Neo4j's Query API v2, with parameters, transactions and status codes.
httpcli
Before you start: Install relate
This guide shows you how to serve a relate store over 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 with relate serve --http <port>. A client sends one POST holding a Cypher 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 as JSON to /db/<namespace>/query/v2 and gets one JSON answer back. The request and the answer follow Neo4j's Query API v2, so any HTTP client works, curl included, with no driver.
Prerequisites
- relate on your
PATH:relate --versionprints its version (Install relate). curl, or any HTTP client.- A store to serve. The first step creates
graph.rl.
Create a store to serve
This command writes a new graph.rl holding the shared example graph: three people, two projects, and who works on what.
relate --database ./graph.rl "CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
(grace:Person {name: 'Grace', role: 'engineer'}),
(noor:Person {name: 'Noor', role: 'maintainer'}),
(relate:Project {name: 'relate', language: 'Rust'}),
(tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
(ada)-[:WORKS_ON {since: 2024}]->(relate),
(grace)-[:WORKS_ON {since: 2023}]->(relate),
(grace)-[:WORKS_ON {since: 2022}]->(tck),
(noor)-[:MAINTAINS]->(relate),
(ada)-[:KNOWS]->(grace)"Start the server
Give --http a port; it has no default. The server binds 127.0.0.1 and runs until SIGINT or SIGTERM:
relate serve --http 7474 --database ./graph.rl --read-onlyTo allow writes, remove --read-only.
On stderr, relate prints that the store is open read-only, then auth: generated token for user relate: <token>, auth: persisted at ./graph.rl.auth-token (reused on restart), and serve: HTTP Query API v2 listening on http://127.0.0.1:7474 (auth: basic). Every request to a statement route needs that credential as HTTP Basic authentication. The token file holds one line, relate:<token>, which is the form curl -u takes. Authentication and TLS covers setting your own credential.
GET / is the one route that needs no credential. It returns the discovery document: the statement endpoint under query, the transaction endpoint under transaction, the relate version under neo4j_version, neo4j_edition community, and bolt_direct when the same process also serves Bolt. Verify shows a whole one.
Run a statement
POST a JSON body with Content-Type: application/json to /db/neo4j/query/v2. neo4j is the default namespace; system and system_ontologies are the other two.
curl -s -u "$(cat graph.rl.auth-token)" -H 'Content-Type: application/json' \
-d '{"statement": "MATCH (p:Person {name: \"Ada\"}) RETURN p"}' \
http://127.0.0.1:7474/db/neo4j/query/v2{"data":{"fields":["p"],"values":[[{"elementId":"0","labels":["Person"],"properties":{"name":"Ada","role":"engineer"}}]]},"bookmarks":[],"timings":{"elapsed_ms":0.410167,"execute_ms":0.235459,"lex_parse_ms":0.030125,"lower_ms":0.039292,"parse_ms":0.111916,"plan_ms":0.052542,"result_generation_ms":0.000209,"rewrite_ms":0.006541},"planCache":"miss"}data.fields names the columns and data.values holds one array per row, in column order. A node An entity in the graph: it carries zero or more labels and a map of properties, and relationships connect it to other nodes. Read more in the glossary is an object with its elementId, labels and properties; a relationship A directed, typed connection between two nodes, with properties of its own; every relationship has exactly one type and a start and an end node. Read more in the glossary adds startNodeElementId, endNodeElementId and type; a path An alternating sequence of nodes and relationships that a pattern matched, bound with p = … and read with nodes(p), relationships(p) and length(p). Read more in the glossary is an array that alternates nodes and relationships. Temporal values are ISO 8601 strings.
| Request field | Required | Meaning |
|---|---|---|
statement | Required | One Cypher statement |
parameters | Optional | An object holding a value for each $name in the statement |
includeCounters | Optional, default false | Adds counters to an answer that changed nothing |
| Answer field | When it is present |
|---|---|
data | Always: fields and values |
counters | After a statement that changed the graph, or with includeCounters: the 11 mutation counters, such as nodes_created and properties_set |
bookmarks | Always, and always []: there is no bookmark manager, because every commit is visible to the next request |
notifications | Only for an EXPLAIN or PROFILE statement that raised one |
queryPlan, profiledQueryPlan | Only for an EXPLAIN or a PROFILE statement |
timings | Whenever a statement ran: its phase timings in milliseconds, such as parse_ms, plan_ms and execute_ms |
planCache | Whenever a statement ran: the plan cache's outcome, such as miss |
timings and planCache come after the members of Neo4j's Query API v2 shape, and their values differ on every request. Unlike the CLI's envelope, the answer carries no column types.
Pass parameters
Put the values in parameters and refer to them as $name:
curl -s -u "$(cat graph.rl.auth-token)" -H 'Content-Type: application/json' \
-d '{"statement": "MATCH (p:Person)-[:WORKS_ON]->(:Project {name: $project}) RETURN p.name AS name, p.role AS role ORDER BY name", "parameters": {"project": "relate"}}' \
http://127.0.0.1:7474/db/neo4j/query/v2{"data":{"fields":["name","role"],"values":[["Ada","engineer"],["Grace","engineer"]]},"bookmarks":[],"timings":{"elapsed_ms":0.37425,"execute_ms":0.25945900000000005,"lex_parse_ms":0.019792,"lower_ms":0.02575,"parse_ms":0.064625,"plan_ms":0.042792,"result_generation_ms":0.000334,"rewrite_ms":0.002167},"planCache":"miss"}Under application/json, a parameter is plain JSON: a string, a number, a boolean, a list, or a map. A date arrives as its string, so build the typed value in Cypher: RETURN date($day) AS day with {"day": "2026-11-07"}. The other request type, application/vnd.neo4j.query.v1.2, also takes the typed form Neo4j's official clients send, such as {"$type": "Integer", "_value": "41"}. A statement that names a parameter the request leaves out fails with 400 and code 42N02; it is never bound as null.
To get typed values back, ask for them with Accept: application/vnd.neo4j.query. Each value then arrives as {"$type": …, "_value": …}, such as {"$type":"Date","_value":"2026-11-07"}, with integers as strings inside _value.
Write in a transaction
Each request to /db/neo4j/query/v2 runs as its own transaction and commits when it succeeds. To group several statements, use the transaction routes. A read-only server serves them too, but a write staged in a transaction there fails at the commit with 400 and code 42N18, and the store is unchanged. This command opens graph.rl for writing:
relate serve --http 7474 --database ./graph.rlBegin a transaction with a POST to …/query/v2/tx. The body's statement is optional, and the answer is 201 with a transaction block:
curl -s -u "$(cat graph.rl.auth-token)" -H 'Content-Type: application/json' \
-d '{"statement": "CREATE (:Person {name: \"Tess\", role: \"engineer\"})"}' \
http://127.0.0.1:7474/db/neo4j/query/v2/tx{"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":0.4075,"execute_ms":0.23766600000000002,"lex_parse_ms":0.004958,"lower_ms":0.004916,"parse_ms":0.162292,"plan_ms":0.002125,"result_generation_ms":0.000166,"rewrite_ms":0.00033299999999999996},"planCache":"miss","transaction":{"expires":"2026-11-10T01:58:10.021Z","id":"tx-0"}}The id, the expires time and the timings differ on every run. Use the id in the routes that follow:
| Method and path | Does | Answers |
|---|---|---|
POST /db/<namespace>/query/v2/tx | Begins a transaction, running the body's statement if it has one | 201, with transaction.id and transaction.expires |
POST /db/<namespace>/query/v2/tx/<id> | Runs a statement in the transaction; an empty body {} only keeps it alive | 200, with the transaction block |
POST /db/<namespace>/query/v2/tx/<id>/commit | Runs the body's statement if it has one, then commits | 200, with no transaction block |
DELETE /db/<namespace>/query/v2/tx/<id> | Rolls back | 202 |
Inside the transaction, its own writes are visible; to other requests they are not, until the commit. A transaction idle for 60 seconds expires and rolls back (--tx-idle-timeout-secs), and at most 128 can be open at once (--max-open-transactions). With "accessMode": "READ" in the body of the request that begins a transaction, a write in it fails with writing is not allowed in a READ transaction and the transaction stays open.
Read the status codes
A failed request answers with a status code and an errors array holding one code and message:
| Status | code | Cause |
|---|---|---|
400 | 42N06 | A body that is not JSON or lacks statement, or a Cypher syntax error |
400 | 42N02 | A parameter the statement names is missing from parameters |
400 | 22G03 | A type error, such as type error: / by zero |
400 | 42N18 | A write on a server started --read-only |
400 | 42000 | Any other statement error |
401 | 42NFF | No credential, or the wrong one |
404 | 42N00 | An unknown namespace or route |
404 | 42000 | An unknown transaction id |
404 | 42N90 | A transaction that expired while idle |
405 | 42000 | The wrong method for the route |
415 | 42000 | A POST without a JSON content type |
429 | 42N91 | A new transaction while 128 are open |
The HTTP errors list every code with its message.
Use it from Neo4j tools
The request and answer shapes are those of Neo4j's Query API v2 statement and transaction endpoints. A client built for that API talks to relate unchanged, within the Cypher relate supports (differences from Neo4j). relate adds one thing, gzip compression for an answer of 1 KiB or more when the request sends Accept-Encoding: gzip. It leaves out batching, a bookmark manager, and JSON Lines answers.
A browser page may call the server only from an allowed origin. By default those are the loopback origins and the hosted Neo4j Browser and Neo4j Console, https://browser.neo4j.io and https://console.neo4j.io. --origin <url> replaces that list and repeats for each origin.
Verify
This step starts the server read-only on port 8842, reads the discovery document, runs a statement with a parameter, tries a write, and sends a request without a credential. Then it stops the server:
relate serve --http 8842 --database ./graph.rl --read-only 2> serve.log &
server=$!
until curl -s -o /dev/null http://127.0.0.1:8842/; do sleep 0.1; done
curl -s -w '\n' http://127.0.0.1:8842/
curl -s -u "$(cat graph.rl.auth-token)" -H 'Content-Type: application/json' \
-d '{"statement": "MATCH (p:Person)-[:WORKS_ON]->(:Project {name: $project}) RETURN p.name AS name ORDER BY name", "parameters": {"project": "relate"}}' \
-w '\n%{http_code}\n' http://127.0.0.1:8842/db/neo4j/query/v2
curl -s -u "$(cat graph.rl.auth-token)" -H 'Content-Type: application/json' \
-d '{"statement": "CREATE (:Person {name: $name})", "parameters": {"name": "Eve"}}' \
-w '\n%{http_code}\n' http://127.0.0.1:8842/db/neo4j/query/v2
curl -s -H 'Content-Type: application/json' -d '{"statement": "RETURN 1 AS ok"}' \
-w '\n%{http_code}\n' http://127.0.0.1:8842/db/neo4j/query/v2
kill $server
wait $server{"query":"http://127.0.0.1:8842/db/{databaseName}/query/v2","transaction":"http://127.0.0.1:8842/db/{databaseName}/query/v2/tx","neo4j_version":"0.1.2","neo4j_edition":"community","auth_config":{"oidc_providers":[]}}
{"data":{"fields":["name"],"values":[["Ada"],["Grace"]]},"bookmarks":[],"timings":{"elapsed_ms":0.658791,"execute_ms":0.472333,"lex_parse_ms":0.034958,"lower_ms":0.043625000000000004,"parse_ms":0.117958,"plan_ms":0.059125000000000004,"result_generation_ms":0.00025,"rewrite_ms":0.006792},"planCache":"miss"}
200
{"errors":[{"code":"42N18","message":"semantic error: WriteOnReadOnlyAccessDatabase: No write operations are allowed on this database. The database is in read-only mode on this Neo4j instance."}]}
400
{"errors":[{"code":"42NFF","message":"authentication required"}]}
401The read returned Ada and Grace with 200, the write was refused with 400 and the store is unchanged, and the request without a credential got 401.
If it goes wrong
401 with code 42NFF and authentication required. The request has no credential or the wrong one. Send -u "$(cat <store>.auth-token)", or the user:password from --auth or RELATE_AUTH.
415 with a message that begins the request content type is browser-"simple". The POST has no JSON content type; curl -d sends application/x-www-form-urlencoded unless told otherwise. Add -H 'Content-Type: application/json'.
400 with code 42N18 and WriteOnReadOnlyAccessDatabase. The server opened the store --read-only. Restart it without --read-only to accept writes.
404 with code 42N00 and semantic error: DatabaseNotFound: Graph not found: <name>. The path names a namespace other than neo4j, system or system_ontologies.
Related
- HTTP server reference: every route, header, and status code.
relate serve: every flag, such as the transaction limits.- Authentication and TLS: credentials and TLS in front of the server.
- Serve over Bolt: the same store for Neo4j drivers.
- Share one store between two processes: when one server should own the store.
- Shared store across processes: the problem this page solves.