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 409 becomes a version-conflict exception, a 404 a 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/http to 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

Per-language guides: Java, Python, JavaScript, Go, .NET, Ruby, PHP, and Rust.

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.