Skip to content
Telemetry
Browse docs

API referenceUpdated October 1, 20266 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

These examples use fictional data in the reserved telemetry_quickstart sample table. They verify API behavior, not application instrumentation. For your application, choose a workflow table and send events from actual operations.

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 telemetry_quickstart:

QUERY=$(cat <<'SQL'
SELECT
  city,
  AVG(price) AS average_price
FROM
  telemetry_quickstart
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
      telemetry_quickstart
    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 telemetry_quickstart'
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 telemetry_quickstart'

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: "parquet", realtime: true, json: true}')") || exit 1

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

# 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 export."
      exit 1
    fi
    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 --fail-with-body -sS --max-time 300 -o telemetry_quickstart_export.parquet "$DOWNLOAD_URL" || exit 1
    echo "Export complete: telemetry_quickstart_export.parquet"
    exit 0
  elif [ "$QUERY_STATUS" = "failed" ] || [ "$QUERY_STATUS" = "cancelled" ]; then
    echo "Export $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 "Export polling timed out."
exit 1

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