Call an approved Request
Last updated:
Read as MarkdownAn 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:
https://<region>.ekaya.ai/api/data/v1/projects/<projectId>
Send your Data Access Point key 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 records headers and trailers; libcurl documents 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.
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, 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.
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. This example uses node:https to check the trailer: response.trailers is populated after the end event.
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 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. 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. Retry decisions, reconciliation, and composing operations belong to the application.