Skip to content
Telemetry
Browse docs
API referenceUpdated July 29, 2026Reviewed by the Telemetry editorial and product teams6 min read

Use this doc with your coding agent

Copy an instrumentation prompt into your coding agent and adapt it to your application.

On this page
  1. Example usage with cURL
  2. Using the JavaScript SDK
  3. Async query
  4. How it works
  5. API reference
  6. Async query with cURL
  7. Example: exporting an entire table
  8. SDK boundary
  9. Common errors

Query

The Query API lets you run SQL against your Telemetry data.

POST https://api.telemetry.sh/query

Headers

Name Type Description
Content-Type String application/json
Authorization String Your API key, either as the raw key or as Bearer <key>

Body

Name Type Required Description
query String Yes SQL query to execute.
realtime Boolean No Defaults to true. Passed through to the query service.
json Boolean No Defaults to true. Passed through to the query service.

Successful response, 200 OK

{
  "status": "success",
  "data": [
    {
      "city": "paris",
      "average_price": 42.0
    }
  ],
  "key_order": ["city", "average_price"]
}

data contains the result rows. key_order preserves the column order from the query result.

Example usage with cURL

This cURL request queries the average ride price by city in uber_rides:

QUERY=$(cat <<'SQL'
SELECT
  city,
  AVG(price) AS average_price
FROM
  uber_rides
GROUP BY
  city
LIMIT
  10000;
SQL
)

curl -X POST https://api.telemetry.sh/query \
  -H "Content-Type: application/json" \
  -H "Authorization: $API_KEY" \
  -d "$(jq -n --arg query "$QUERY" '{query: $query, realtime: true, json: true}')"

Using the JavaScript SDK

We recommend using the SDKs for a better developer experience:

import telemetry from "telemetry-sh";

telemetry.init("YOUR_API_KEY");

const results =
  await telemetry.query(`
    SELECT
      city,
      AVG(price)
    FROM
      uber_rides
    GROUP BY
      city
  `);

Async query

Small JSON results are returned inline in result, with data and key_order. Larger JSON results and Parquet exports use download_url. A completed response needs only one of these; inline results do not require a download URL or download URL expiry fields. If download_url_expired is true, start a new query. Treat expiry fields as optional, rather than requiring them to recognize a completed inline result.

This is especially useful for exporting entire tables, running heavy aggregations, or downloading results as JSON or Parquet files.

How it works

  1. POST your query to /query/async. The server returns a job_id and status_url.
  2. GET the status_url to check progress.
  3. Read results from inline result, or download the file from download_url.

API reference

Start an async query

POST https://api.telemetry.sh/query/async

Headers

Name Type Description
Content-Type String application/json
Authorization String Your API key, either as the raw key or as Bearer <key>

Body

Name Type Required Description
query String Yes SQL query to execute.
realtime Boolean No Defaults to true. Passed through to the query service.
json Boolean No Defaults to true. Passed through to the query service.
format String No Result format, "json" by default or "parquet".

Response, 202 Accepted

{
  "status": "accepted",
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "format": "json",
  "status_url": "/query/async/550e8400-e29b-41d4-a716-446655440000"
}

Poll query status

GET https://api.telemetry.sh/query/async/{job_id}

Headers

Name Type Description
Authorization String Your API key, either as the raw key or as Bearer <key>

Response, 200 OK

Inline JSON result

{
  "status": "success",
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "query_status": "completed",
  "format": "json",
  "progress_pct": 100,
  "result": {
    "data": [{ "events": 7 }],
    "key_order": ["events"]
  }
}

Result available as a download

{
  "status": "success",
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "query_status": "completed",
  "format": "json",
  "progress_pct": 100,
  "message": "Query completed",
  "created_at": "2025-02-24T12:00:00Z",
  "completed_at": "2025-02-24T12:00:05Z",
  "download_url": "https://storage.example.com/results/...",
  "download_url_expires_in_seconds": 3600
}

When format is "json", the downloaded file contains query metadata plus the standard query result shape under result:

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "format": "json",
  "result": {
    "data": [
      {
        "city": "paris",
        "average_price": 42.0
      }
    ],
    "key_order": ["city", "average_price"]
  }
}

The query_status field will be one of:

Status Description
queued The query is waiting to be executed
running The query is currently executing
completed Results are ready in result or via download_url.
failed The query failed. Check the error field.
cancelled The query was cancelled.

Async query with cURL

# 1. Start the async query
QUERY='SELECT * FROM uber_rides'
RESPONSE=$(curl --fail-with-body -sS --max-time 30 https://api.telemetry.sh/query/async \
  -H "Content-Type: application/json" \
  -H "Authorization: $API_KEY" \
  -d "$(jq -n --arg query "$QUERY" '{query: $query, format: "json", realtime: true, json: true}')") || exit 1
STATUS_URL="https://api.telemetry.sh$(echo "$RESPONSE" | jq -r '.status_url')"

# 2. Poll for at most ten minutes
DEADLINE=$((SECONDS + 600))
while [ "$SECONDS" -lt "$DEADLINE" ]; do
  STATUS=$(curl --fail-with-body -sS --max-time 30 -H "Authorization: $API_KEY" "$STATUS_URL") || exit 1
  QUERY_STATUS=$(echo "$STATUS" | jq -r '.query_status')

  if [ "$QUERY_STATUS" = "completed" ]; then
    if echo "$STATUS" | jq -e '.download_url_expired == true' >/dev/null; then
      echo "Result expired. Start a new query."
      exit 1
    elif echo "$STATUS" | jq -e '.result | type == "object"' >/dev/null; then
      echo "$STATUS" | jq '{result: .result}' > results.json
    else
      DOWNLOAD_URL=$(echo "$STATUS" | jq -r '.download_url // empty')
      if [ -z "$DOWNLOAD_URL" ]; then
        echo "Completed response has neither a result nor an active download URL."
        exit 1
      fi
      curl --fail-with-body -sS --max-time 300 -o results.json "$DOWNLOAD_URL" || exit 1
    fi
    # Both branches have the same result shape, including an empty data array.
    jq '.result' results.json
    exit 0
  elif [ "$QUERY_STATUS" = "failed" ] || [ "$QUERY_STATUS" = "cancelled" ]; then
    echo "Query $QUERY_STATUS: $(echo "$STATUS" | jq -r '.error // .message')"
    exit 1
  elif [ "$QUERY_STATUS" != "queued" ] && [ "$QUERY_STATUS" != "running" ]; then
    echo "Unexpected query status: $QUERY_STATUS"
    exit 1
  fi
  sleep 5
done
echo "Query polling timed out."
exit 1

Example: exporting an entire table

Use the async query API for full table exports. It has no row limit and returns a single downloadable file.

# Start the export as Parquet
QUERY='SELECT * FROM uber_rides'

RESPONSE=$(curl -s -X POST https://api.telemetry.sh/query/async \
  -H "Content-Type: application/json" \
  -H "Authorization: $API_KEY" \
  -d "$(jq -n --arg query "$QUERY" '{query: $query, format: "parquet", realtime: true, json: true}')")

STATUS_URL="https://api.telemetry.sh$(echo "$RESPONSE" | jq -r '.status_url')"

# Poll until complete
while true; do
  STATUS=$(curl -s -H "Authorization: $API_KEY" "$STATUS_URL")
  QUERY_STATUS=$(echo "$STATUS" | jq -r '.query_status')

  if [ "$QUERY_STATUS" = "completed" ]; then
    DOWNLOAD_URL=$(echo "$STATUS" | jq -r '.download_url // empty')
    if [ -z "$DOWNLOAD_URL" ]; then
      echo "Export completed, but no active download URL is available."
      exit 1
    fi
    curl -o uber_rides_export.parquet "$DOWNLOAD_URL"
    echo "Export complete: uber_rides_export.parquet"
    break
  elif [ "$QUERY_STATUS" = "failed" ]; then
    echo "Export failed: $(echo "$STATUS" | jq -r '.error // .message')"
    exit 1
  fi

  sleep 5
done

SDK boundary

The current JavaScript SDK's query method calls the interactive /query endpoint. It does not start /query/async, poll status_url, enforce an asynchronous-job timeout, or download the finished artifact.

Use the HTTP start, poll, and download flow above for large JSON or Parquet exports. An application wrapper may encapsulate those three operations, but it should still enforce a total polling deadline, stop on failed, and treat an expired download_url as a new export rather than retrying the old download indefinitely.

Common errors

  • 400 Bad Request if the JSON body is invalid
  • 400 Bad Request if query is missing or empty
  • 401 Unauthorized if the API key is missing or invalid
  • 402 Payment Required if the account is blocked by a paywall check
  • 429 Too Many Requests if the API key exceeds the gateway rate limit

Related feature

Run read-only DataFusion SQL over structured-event tables and reuse the result.

Page authors and references

The Telemetry editorial team maintains this page. The product team checks the examples and confirms how the product behaves.

How we review our docs