# Call an approved Request

Last updated: 2026-10-11

An Approved Request defines an operation and its parameter contract. A Query reads; an Action changes something. The caller supplies parameters for the approved operation.

## Discover the contract

Use the Project's MCP URL to identify its Region and Project identifier. The HTTP Data API uses this base URL shape:

```text
https://<region>.ekaya.ai/api/data/v1/projects/<projectId>
```

Send your [Data Access Point key](/docs/data-access-points-and-keys/) as `Authorization: Bearer <Data Access Point key>`.

| Method and path, relative to the base URL | Purpose |
| --- | --- |
| `GET /catalog` | Discover approved Requests granted to the caller. |
| `GET /catalog.openapi` | Read the caller's generated OpenAPI description. |
| `POST /requests/<requestId>/execute` | Execute the selected Request with its declared parameters. |

You can select a Request in the OpenAPI description with `?request=<requestId>`. Selection does not grant access to an otherwise unavailable Request.

Over MCP, `list_approved_requests` returns HTTP details for granted Requests. With `ids` and `openapi: true`, it also returns a bounded OpenAPI description; check `openapi_omitted_ids` for granted operations that did not fit. Unknown or ungranted identifiers are omitted. Use the returned contract; names, parameters, and results differ between Projects.

## Example: read by region

These examples assume an Administrator has approved a **Query** with a required string parameter named `region`, and has granted it to your Data Access Point. This is an illustrative contract, not a preinstalled Request. Use a real Request identifier from your catalog and adapt the parameters to its definition.

Set `EKAYA_DATA_URL` to the base URL above with the placeholders replaced, `EKAYA_REQUEST_ID` to the approved Query's identifier, and `EKAYA_API_KEY` from your secret store. Each example makes a single call and does not retry.

All three Query examples wait for the response to finish and require HTTP success, `outcome: "succeeded"`, and the terminal `X-Ekaya-Stream-Status: complete` trailer. A missing trailer or any other value means completeness has not been established. They also reject `mocked: true`, since complete generated data is not a live result. They omit the caller's `limit` because they need a completeness verdict, and print rejected bodies as diagnostics, never as accepted results.

### curl

Run as a shell script with curl and jq installed. curl's [`--dump-header`](https://curl.se/docs/manpage.html#-D) records headers and trailers; [libcurl documents](https://curl.se/libcurl/c/CURLOPT_HEADERFUNCTION.html) how the blank line after the response headers distinguishes trailers. This example requests HTTP/1.1 and checks only the trailer section after that blank line.

```sh
set -eu
response_dir=$(mktemp -d)
trap 'rm -rf "$response_dir"' EXIT

if ! http_status=$(curl --disable --silent --show-error --http1.1 \
  --dump-header "$response_dir/headers" \
  --output "$response_dir/body" --write-out '%{http_code}' \
  --request POST \
  "${EKAYA_DATA_URL%/}/requests/${EKAYA_REQUEST_ID}/execute" \
  --header "Authorization: Bearer ${EKAYA_API_KEY}" \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data '{"parameters":{"region":"emea"}}'); then
  cat "$response_dir/body" >&2
  exit 1
fi

# Reset on each status line (including proxy or informational responses).
stream_status=$(awk '
  BEGIN { name_field = 1; value_field = 2 }
  { sub(/\r$/, "") }
  /^HTTP\// { headers = 1; count = 0; value = ""; next }
  /^$/ { headers = 0; next }
  !headers && tolower($name_field) == "x-ekaya-stream-status:" {
    count++; value = NF == 2 ? $value_field : ""
  }
  END { if (count == 1) print value }
' "$response_dir/headers")

case "$http_status" in
  2??) ;;
  *) cat "$response_dir/body" >&2; exit 1 ;;
esac
if [ "$stream_status" != complete ] || \
  ! jq -e '.outcome == "succeeded" and .mocked != true' "$response_dir/body" >/dev/null; then
  echo "Result not accepted; stream status: ${stream_status:-missing}" >&2
  cat "$response_dir/body" >&2
  exit 1
fi
cat "$response_dir/body"
```

### Python

Python's standard `http.client` [discards trailers](https://github.com/python/cpython/blob/3.14/Lib/http/client.py), including when used through `urllib.request`. This example uses standard-library subprocess and JSON handling, with **curl installed separately**, to capture and check the trailers. It does not use `http.client` to infer completeness from the body.

```python
import json
import os
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory

url = (
    os.environ["EKAYA_DATA_URL"].rstrip("/")
    + "/requests/" + os.environ["EKAYA_REQUEST_ID"] + "/execute"
)
with TemporaryDirectory() as directory:
    headers_path = Path(directory) / "headers"
    body_path = Path(directory) / "body"
    response = subprocess.run([
        "curl", "--disable", "--silent", "--show-error", "--http1.1",
        "--dump-header", str(headers_path),
        "--output", str(body_path), "--write-out", "%{http_code}",
        "--request", "POST", url,
        "--header", "Authorization: Bearer " + os.environ["EKAYA_API_KEY"],
        "--header", "Content-Type: application/json",
        "--header", "Accept: application/json",
        "--data", json.dumps({"parameters": {"region": "emea"}}),
    ], capture_output=True, text=True)
    body = body_path.read_text() if body_path.exists() else ""
    if response.returncode != 0:
        raise SystemExit(response.stderr + body)

    trailers = {}
    in_headers = True
    for line in headers_path.read_text().splitlines():
        if line.startswith("HTTP/"):
            in_headers = True
            trailers = {}
        elif not line:
            in_headers = False
        elif not in_headers:
            name, separator, value = line.partition(":")
            if separator:
                trailers.setdefault(name.lower(), []).append(value.strip())

    stream_status = trailers.get("x-ekaya-stream-status", [])
    if not 200 <= int(response.stdout) < 300 or stream_status != ["complete"]:
        raise SystemExit(f"Result not accepted; trailers: {trailers}\n{body}")
    result = json.loads(body)
    if result.get("outcome") != "succeeded" or result.get("mocked") is True:
        raise SystemExit(body)
    print(json.dumps(result, ensure_ascii=False))
```

### Node

Run as an ES module in Node. Its built-in `fetch` [Response does not expose trailers](https://github.com/nodejs/undici/blob/main/types/fetch.d.ts). This example uses `node:https` to check the trailer: [`response.trailers`](https://nodejs.org/api/http.html#messagetrailers) is populated after the `end` event.

```js
import { request } from "node:https";

const { EKAYA_DATA_URL, EKAYA_REQUEST_ID, EKAYA_API_KEY } = process.env;
if (!EKAYA_DATA_URL || !EKAYA_REQUEST_ID || !EKAYA_API_KEY) {
  throw new Error("Set the Project URL, Request identifier, and key.");
}
const url = `${EKAYA_DATA_URL.replace(/\/$/, "")}/requests/${EKAYA_REQUEST_ID}/execute`;
const result = await new Promise((resolve, reject) => {
  const req = request(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${EKAYA_API_KEY}`,
      "Content-Type": "application/json",
      Accept: "application/json",
    },
  }, (res) => {
    let body = "";
    res.setEncoding("utf8");
    res.on("data", (chunk) => { body += chunk; });
    res.on("error", reject);
    res.on("end", () => {
      try {
        const streamStatus = res.trailers["x-ekaya-stream-status"];
        if (res.statusCode < 200 || res.statusCode >= 300 || streamStatus !== "complete") {
          throw new Error(`Result not accepted; trailers: ${JSON.stringify(res.trailers)}\n${body}`);
        }
        const parsed = JSON.parse(body);
        if (parsed.outcome !== "succeeded" || parsed.mocked === true) throw new Error(body);
        resolve(parsed);
      } catch (error) {
        reject(error);
      }
    });
  });
  req.on("error", reject);
  req.end(JSON.stringify({ parameters: { region: "emea" } }));
});
console.log(JSON.stringify(result));
```

## Read the result

### HTTP Data API

With `Accept: application/json`, a tabular Query result contains `columns`, `rows`, `row_count`, `execution_id`, `outcome`, and `stream_status`, plus `mocked: true` for a mock. Each row is a JSON object keyed by column name, with fields emitted in `columns` order. Use the names in `columns` to find values; HTTP rows are not positional arrays.

JSON is the buffered encoding: the answer is gathered and encoded in full before any of it reaches you, so a read past the deployment's bound for that encoding is refused whole with HTTP 413 and error `result_too_large` rather than truncated, and no partial result arrives. Whether an answer can stream depends on what the Request runs against, which the catalog reports in `request_kind`: `statement` is a statement against a database, `operation` is a call to a service. Only a `statement` read streams. For a larger one, request `application/x-ndjson` or `application/vnd.apache.arrow.stream` in `Accept`: those encodings stream, so their rows arrive without the JSON envelope described below, and an extract that a streaming bound cuts short is reported in the terminal `X-Ekaya-Stream-Status` trailer instead of as a status code. An `operation` read is answered whole in whatever encoding was negotiated, so even with `application/x-ndjson` or `application/vnd.apache.arrow.stream` it can be refused the same way as a JSON read — HTTP 413 and error `result_too_large` before any of the answer arrives — rather than arriving cut short with a trailer. The bound is deployment configuration and this site publishes no figure for it; where another encoding still cannot carry the answer, narrow the extract with the Request's own parameters.

The HTTP JSON body has no `truncated` boolean. A Query's buffered JSON rows or document answer, including an empty result, carries `stream_status`, using the same vocabulary as the terminal `X-Ekaya-Stream-Status` trailer. Accept that read answer as complete only when `stream_status === "complete"`, alongside HTTP success and `outcome: "succeeded"`. Treat `truncated`, `failed`, `timeout`, a missing field, or any unknown value as not complete. HTTP 200 and `row_count` alone do not establish completeness. Also check `mocked`: a mocked read reports `stream_status: "complete"` beside `mocked: true`, but its generated data proves nothing about a real execution.

The effective row ceiling is the lowest of the applicable deployment ceiling, the approved contract's row limit, and the caller's own `limit`. Reaching it can return HTTP 200 with valid rows and `stream_status: "truncated"`; this is a partial result, not a failed execution or the HTTP 413 refusal described above. Omit `limit` when you need a completeness verdict. If the result is `truncated`, tell the viewer it is partial, avoid presenting a full table or computing totals from it, and narrow the Request's parameters before reading again.

An Action's JSON summary carries `rows_affected`, `batch_size`, `row_count`, `execution_id`, and `outcome`, with returned `columns` and `rows` or a `document` when applicable. It has no `stream_status`, even when it returns data; the read completeness rule does not apply. Check HTTP success, the outcome, and `mocked`. HTTP 202 or `outcome: "indeterminate"` means the effect is uncertain; keep the execution evidence and follow [Errors](/docs/errors/) before considering another call.

For streaming read encodings, read the terminal `X-Ekaya-Stream-Status` trailer after consuming the body: only `complete` establishes completeness. Treat any other value or a missing trailer as not complete. The `Trailer` header declares `X-Ekaya-Stream-Status`, `X-Ekaya-Row-Count`, and `X-Ekaya-Execution-Id`; their terminal values report completeness, delivered row count, and execution evidence. The declaration alone is not a completion signal. `X-Ekaya-Mocked: true` marks generated data even when the status is `complete`. The examples above also check the completeness trailer for JSON responses and reject the body's mock marker.

Browser `fetch` cannot read trailers. For a Query, request buffered JSON with `Accept: application/json` and check its `stream_status` and `mocked` fields as above; see the same-origin [Hosted Application example](/docs/hosted-applications/). A client or intermediary that discards trailers cannot establish completeness for streaming encodings.

Some Queries return a `document` instead of `columns` and `rows`. In HTTP JSON, this envelope has `row_count: 1`, `stream_status`, and execution evidence. Apply the same completeness check to documents and empty read answers. With HTTP newline-delimited JSON, a document is the single JSON line itself; tabular results are row objects, one per line, without the JSON envelope. MCP's row-format setting does not change HTTP `Accept` negotiation.

### MCP

The MCP tool `execute_approved_request` takes `query_id` and `parameters`. Its tabular Query format defaults to `compact`: each row is a positional array, with its values in `columns` order. `format: "records"` requests row objects keyed by column name, with declared fields in `columns` order.

MCP tabular Query results carry a body-level `truncated` boolean. Check that it is `false`, alongside the outcome, before treating those results as complete. This is separate from the HTTP Data API trailer. MCP document results use `document` instead of `columns` and `rows`, with `row_count: 1`; they do not carry the tabular `truncated` field.

On either interface, a document's fields follow the Request's output. If it contains a file link, treat that link as provider data; do not send your Ekaya key to that host or assume a lifetime for the link.

SQL Request authors declare named value parameters with `{{name}}`. Callers pass the name in `parameters`, without the braces. Use the approved parameter definitions and let Ekaya supply identity-bound parameters. Review the SQL's actual tables, columns, and predicates when authoring a Request; a parameter name alone does not establish its data scope.

For an Action, a missing response or `indeterminate` outcome is not permission to repeat it. Keep the execution evidence and follow [Errors](/docs/errors/). Retry decisions, reconciliation, and composing operations belong to the application.
