Clients & REST API conventions
|
This section documents the current Elasticsearch 9.x line (with 8.19 as the final 8.x release) as published at the Elasticsearch documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Kibana-only UIs, the ML/NLP model-management workflow, cross-cluster replication, and parts of the paid / serverless-only surface) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Elasticsearch iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
Every Elasticsearch operation is an HTTP request with a JSON body, so any HTTP client can drive a
cluster. Elastic also ships an official client for each major language that adds a managed
transport, typed request and response objects, and built-in authentication. This page covers what
those clients share, when to reach for one instead of raw REST, and the request/response conventions — shared by clients and curl alike — that the rest of these pages assume. For the Console
shorthand used in [source,console] examples and a first round-trip, see
Getting started.
The official language clients
Elastic maintains clients for Java (the typed ElasticsearchClient), Python,
JavaScript/TypeScript, Go, .NET, Ruby, PHP, and Rust. They are generated from the same
machine-readable API specification, so method names and parameters line up across languages and
track each Elasticsearch release. The catalogue is
Elasticsearch clients.
What every client shares
-
HTTP transport. A single low-level transport sends requests, sets the JSON content type, applies authentication headers, and maps HTTP status codes to typed errors (a
409becomes a version-conflict exception, a404a not-found result). -
Connection pooling and failover. The client holds a pool of node connections, round-robins requests across them, marks a node dead on a connection error, retries the request on another node, and revives dead nodes on a backoff schedule.
-
Sniffing (opt-in). The client can periodically call
GET /_nodes/httpto discover the cluster’s current node list and update its pool, so nodes added or removed after start-up are picked up without a config change. Disable it behind a load balancer or when nodes advertise unreachable addresses. See Sniffer for the Java implementation. -
Typed requests and responses. Instead of hand-built JSON strings, you build a request object with a fluent or lambda builder and get back a parsed response; in Java and .NET the search hits deserialize into your own domain class.
// Java: the typed client sits on a transport (RestClient) + a JSON mapper.
RestClient restClient = RestClient
.builder(new HttpHost("localhost", 9200, "https"))
.build();
ElasticsearchTransport transport =
new RestClientTransport(restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);
// Request and response are generated types; hits deserialize into Book.
SearchResponse<Book> res = client.search(s -> s
.index("books")
.query(q -> q.match(m -> m.field("title").query("darkness"))),
Book.class);
res.hits().hits().forEach(h -> System.out.println(h.source()));
// https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/connecting.html
# Python: same operation, same parameter names.
from elasticsearch import Elasticsearch
es = Elasticsearch("https://localhost:9200", basic_auth=("elastic", "PASSWORD"))
resp = es.search(index="books", query={"match": {"title": "darkness"}})
for hit in resp["hits"]["hits"]:
print(hit["_source"])
# https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/connecting.html
Client or raw REST?
Use an official client in application code: you get connection pooling, retries and failover, one place for auth and TLS, typed errors, and telemetry hooks — none of which you want to reimplement around a bare HTTP call.
Use raw REST (curl, the Kibana Dev Tools Console, a shell script, or a language with no official
client) for ad-hoc administration, one-off queries, reproducing a request in a bug report, or _cat
output meant for human eyes. The [source,console] snippets in these pages are raw REST for exactly
that reason.
# The same search as above, no client, for a quick check from a shell.
curl -u elastic:$ES_PASSWORD --cacert http_ca.crt \
-H 'Content-Type: application/json' \
'https://localhost:9200/books/_search?pretty' \
-d '{ "query": { "match": { "title": "darkness" } } }'
# https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html
For SQL-like and pipe-based querying (Elasticsearch SQL and ES|QL) that also runs over these same endpoints, see Query languages & scripting.
Elastic Cloud and a secured cluster
For Elastic Cloud, the deployment page gives you a cloud id — a single base64 token that encodes the cluster endpoint and port. Every client accepts it directly instead of a URL:
es = Elasticsearch(
cloud_id="my-deployment:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvJGFiYzEyMyRkZWY0NTY=",
api_key="VnVhQ2ZHY0JDZFJrc...",
)
# https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html
Authenticate to any secured cluster (self-managed or Cloud) with an API key rather than a username
and password: it is scoped to specific indices and privileges, can carry an expiration, and is
revocable without touching a user account. Create one, then pass its encoded value to the client
(clients send it as the Authorization: ApiKey <encoded> header):
POST /_security/api_key
{
"name": "books-app",
"expiration": "30d",
"role_descriptors": {
"books-ro": {
"indices": [ { "names": ["books*"], "privileges": ["read"] } ]
}
}
}
// use response.encoded as the API key in the client
// https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html
curl -H 'Authorization: ApiKey VnVhQ2ZHY0JDZFJrc...' https://localhost:9200/
# https://www.elastic.co/guide/en/elasticsearch/reference/current/http-clients.html
API keys, roles and the enrollment/TLS flow are covered on Security.
REST API conventions
The conventions below apply to every endpoint and are documented in API conventions and Common options.
Multi-target syntax and wildcards
Any endpoint that takes an index name accepts a comma-separated list, a wildcard, exclusions with a
leading -, and the special target _all:
GET /logs-2024-*,logs-2025-*/_search // union of two wildcard sets
GET /_all/_count // every index (same as *)
GET /logs-*,-logs-archive-*/_search // all logs-* except the archive ones
GET /my-a,my-b,my-c/_search // an explicit list
// https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-multi-index
expand_wildcards controls which index states a wildcard resolves to: open (default), closed,
hidden, none, or all. Hidden indices and data-stream backing indices are not matched unless
you ask:
GET /logs-*/_search?expand_wildcards=open,hidden
GET /*/_search?expand_wildcards=all&ignore_unavailable=true&allow_no_indices=true
// https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-multi-index
Date math in index names
An index name wrapped as <static-name-{date-math-expr}> is resolved against the current time at
request time — handy for writing to or reading a bounded set of time-based indices without
computing the date client-side. In the Console you can pass the literal form; over curl the <,
>, {, } and / characters must be URL-encoded:
# <logs-{now/d}> -> logs-2025.09.06 for a request made today (now/d = start of day, UTC)
POST /%3Clogs-%7Bnow%2Fd%7D%3E/_doc
{ "@timestamp": "2025-09-06T10:00:00Z", "message": "hello" }
# custom format and time zone: <logs-{now/d{yyyy.MM.dd|-07:00}}>
GET /%3Clogs-%7Bnow%2Fd%7Byyyy.MM.dd%7C-07%3A00%7D%7D%3E,%3Clogs-%7Bnow%2Fd-1d%7Byyyy.MM.dd%7C-07%3A00%7D%7D%3E/_search
// today and yesterday's index, resolved server-side
// https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-date-math-index-names
Common query parameters
These work on nearly every request:
GET /books/_search?pretty // indent the JSON response (dev only)
GET /books/_search?human=false // sizes/durations as raw numbers, not "1.2gb" / "3.5s"
GET /books/_search?filter_path=hits.hits._source // return only these response paths
GET /_cluster/state?filter_path=metadata.indices.*.state,-metadata.indices.*.settings
GET /books/_search?error_trace=true // include the server-side stack trace on error
// https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html
filter_path accepts wildcards ( within a path segment, * across segments) and exclusions
with a leading -; trimming the response this way is the cheapest way to cut network cost on large
result sets.
_cat output: v, h, s, format
The compact and aligned text (cat) APIs are for humans at a terminal and take a few extra parameters:
GET /_cat/indices?v // v = print a header row
GET /_cat/indices?v&h=index,docs.count,store.size // h = choose columns (and their order)
GET /_cat/indices?v&s=store.size:desc // s = sort by column(s), :asc / :desc
GET /_cat/nodes?format=json // format = text (default) | json | yaml | cbor | smile
GET /_cat/health?help // list every available column for this endpoint
// https://www.elastic.co/guide/en/elasticsearch/reference/current/cat.html
Never parse text _cat output in code — use the real JSON API for that endpoint, or
format=json.
wait_for_active_shards
Write operations (index, update, delete, _bulk, and index creation) accept
wait_for_active_shards: the number of shard copies — primary plus replicas — that must be active
before the operation proceeds. The default is 1 (primary only); all requires every in-sync copy;
a number requires that many:
PUT /orders/_doc/1?wait_for_active_shards=2&timeout=30s
{ "status": "NEW", "total": 42.00 }
// block until 2 copies are active, or fail after 30s (default timeout 1m).
// https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-wait-for-active-shards
API compatibility and version skew
The REST API compatibility header
To keep an older client working against a newer cluster during an upgrade, send the compatibility
header on both Accept and Content-Type, using the media type
application/vnd.elasticsearch+json with compatible-with=N where N is the previous major
version:
curl -H 'Accept: application/vnd.elasticsearch+json; compatible-with=8' \
-H 'Content-Type: application/vnd.elasticsearch+json; compatible-with=8' \
-u elastic:$ES_PASSWORD --cacert http_ca.crt \
'https://localhost:9200/my-index/_search' \
-d '{ "query": { "match_all": {} } }'
# a 9.x node then accepts 8.x request bodies and returns 8.x-shaped responses;
# syntax removed in 9.x produces a deprecation warning instead of an error.
# https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-api-compatibility.html
The official clients set this header for you when the client’s major version is one behind the cluster, so a released 8.x client keeps working against a 9.x cluster while you upgrade it.
Client and server version skew
The supported combinations are published in the
Elastic Support Matrix. The rule of thumb: a client of major
version X is supported against clusters of major X, and — via the compatibility header — against major X+1 during a migration. Upgrade the cluster first, then the clients. Do not run a
client that is a full major version ahead of the cluster.
The include_type_name history note
There is one document type per index and the write path uses the literal _doc. The
?include_type_name query parameter — a 7.x transitional flag that kept the old typed mapping APIs
working — was removed in 8.0 and has no effect on a 9.x cluster; likewise the pre-7.x
/<index>/<type>/<id> URL form is gone. See
Mapping & field types for the current
model.
Continue with Query languages & scripting.