diff --git a/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusSeriesRestIT.java b/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusSeriesRestIT.java
new file mode 100644
index 0000000000000..2a7d7f132adff
--- /dev/null
+++ b/x-pack/plugin/prometheus/src/javaRestTest/java/org/elasticsearch/xpack/prometheus/PrometheusSeriesRestIT.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.prometheus;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.compression.Snappy;
+
+import org.apache.http.HttpHeaders;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.ContentType;
+import org.apache.http.util.EntityUtils;
+import org.elasticsearch.client.Request;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
+import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.test.cluster.ElasticsearchCluster;
+import org.elasticsearch.test.cluster.local.distribution.DistributionType;
+import org.elasticsearch.test.rest.ESRestTestCase;
+import org.elasticsearch.xpack.prometheus.proto.RemoteWrite;
+import org.junit.ClassRule;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.notNullValue;
+
+/**
+ * Integration tests for the Prometheus {@code GET /api/v1/series} endpoint.
+ *
+ *
Tests focus on high-level HTTP concerns: routing, request/response format, status codes.
+ * Detailed plan-building and response-parsing logic is covered by unit tests.
+ */
+public class PrometheusSeriesRestIT extends ESRestTestCase {
+
+ private static final String USER = "test_admin";
+ private static final String PASS = "x-pack-test-password";
+ private static final String DEFAULT_DATA_STREAM = "metrics-generic.prometheus-default";
+
+ @ClassRule
+ public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
+ .distribution(DistributionType.DEFAULT)
+ .user(USER, PASS, "superuser", false)
+ .setting("xpack.security.enabled", "true")
+ .setting("xpack.security.autoconfiguration.enabled", "false")
+ .setting("xpack.license.self_generated.type", "trial")
+ .setting("xpack.ml.enabled", "false")
+ .setting("xpack.watcher.enabled", "false")
+ .build();
+
+ @Override
+ protected String getTestRestCluster() {
+ return cluster.getHttpAddresses();
+ }
+
+ @Override
+ protected Settings restClientSettings() {
+ String token = basicAuthHeaderValue(USER, new SecureString(PASS.toCharArray()));
+ return Settings.builder().put(super.restClientSettings()).put(ThreadContext.PREFIX + ".Authorization", token).build();
+ }
+
+ // Validation — 400 responses
+
+ public void testMissingMatchSelectorReturnsBadRequest() throws Exception {
+ Request request = new Request("GET", "/_prometheus/api/v1/series");
+ ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request));
+ assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(400));
+ assertThat(EntityUtils.toString(e.getResponse().getEntity()), containsString("match[]"));
+ }
+
+ public void testInvalidSelectorSyntaxReturnsBadRequest() throws Exception {
+ // {not valid!!!} is not valid PromQL
+ Request request = seriesRequest("{not valid!!!}");
+ ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request));
+ assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(400));
+ }
+
+ public void testRangeSelectorReturnsBadRequest() throws Exception {
+ // up[5m] is a range vector, not an instant vector
+ Request request = seriesRequest("up[5m]");
+ ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request));
+ assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(400));
+ }
+
+ // Response format — 200 with JSON envelope
+
+ public void testGetResponseIsJsonWithSuccessEnvelope() throws Exception {
+ writeMetric("test_gauge", Map.of());
+
+ Response response = querySeries("test_gauge");
+
+ assertThat(response.getStatusLine().getStatusCode(), equalTo(200));
+ assertThat(response.getEntity().getContentType().getValue(), containsString("application/json"));
+
+ Map body = entityAsMap(response);
+ assertThat(body.get("status"), equalTo("success"));
+ assertThat(body.get("data"), notNullValue());
+ }
+
+ // Data round-trip — write via remote write, read back via series
+
+ public void testGetReturnsIndexedSeries() throws Exception {
+ writeMetric("test_gauge", Map.of("job", "series_test", "instance", "localhost:9090"), 42.0);
+
+ List