All commands accept an --api-key <key> flag for authentication (except sql). API key resolution order: --api-key flag > APITALLY_API_KEY env var > ~/.apitally/auth.json.
Commands that accept a --db flag use ~/.apitally/data.duckdb as the default database path if no other path is specified. If the database file doesn't exist, it will be created (except for the sql command). When writing to tables, existing records are updated (no duplicates are created).
Datetime flags (e.g. --since, --until, --requests-since) accept ISO 8601 strings or compact relative durations (e.g. 30m, 24h, 7d, 2w).
npx @apitally/cli auth [--api-key <key>]
Opens a browser-based auth flow where the user logs in to the Apitally dashboard and selects a team. A newly created API key is then passed back to the CLI. The key is saved to ~/.apitally/auth.json and used by all subsequent commands unless overridden by the --api-key flag.
If --api-key is provided, the key is saved directly without opening the browser.
npx @apitally/cli whoami
Check authentication and show the team name. Outputs JSON to stdout. Exits with code 3 if not authenticated.
Example output:
{"team":{"id":1,"name":"My Team"}}npx @apitally/cli apps [--db [<path>]]
List all apps in the team. Use this to get app IDs for other commands. Outputs NDJSON to stdout by default.
--db: Write toappsandapp_envstables in DuckDB instead of outputting NDJSON to stdout
Example NDJSON output (without --db):
{"id":1,"name":"Example API 1","framework":"FastAPI","client_id":"76bf09e2-8996-4dd0-bdb5-ccdc3a48f64c","envs":[{"id":1,"name":"prod","created_at":"2026-01-01T00:00:00.000000Z","last_sync_at":"2026-01-01T01:00:00.000000Z"}],"created_at":"2026-01-01T00:00:00.000000Z"}
{"id":2,"name":"Example API 2","framework":"FastAPI","client_id":"339c08bb-5e88-4cba-a24d-be9d80fbd096","envs":[{"id":2,"name":"prod","created_at":"2026-01-02T00:00:00.000000Z","last_sync_at":"2026-01-02T01:00:00.000000Z"}],"created_at":"2026-01-02T00:00:00.000000Z"}npx @apitally/cli consumers <app-id> [--requests-since <datetime>] [--db [<path>]]
List all consumers for an app. Use this to map consumer IDs in request logs to identifiers and names. Outputs NDJSON to stdout by default.
--requests-since: Only return consumers active since this datetime (ISO 8601 or relative duration, e.g. 24h, 7d)--db: Write toconsumerstable in DuckDB instead of outputting NDJSON to stdout
Example NDJSON output (without --db):
{"id":1,"identifier":"bob@example.com","name":"Bob","group":{"id":1,"name":"Admins"},"created_at":"2026-01-01T00:00:00Z","last_request_at":"2026-01-01T01:00:00Z"}
{"id":2,"identifier":"alice@example.com","name":"Alice","group":null,"created_at":"2026-01-02T00:00:00Z","last_request_at":"2026-01-02T02:00:00Z"}npx @apitally/cli endpoints <app-id> [--method <methods>] [--path <pattern>] [--db [<path>]]
List API endpoints for an app, ordered by path and method. Use this to see which endpoints exist for an app. Outputs NDJSON to stdout by default.
--method: Filter to HTTP method(s), comma-separated (e.g.GET,POST)--path: Filter to path pattern, supports wildcards (e.g./v1/*)--db: Write toendpointstable in DuckDB instead of outputting NDJSON to stdout
Example NDJSON output (without --db):
{"id":1,"method":"POST","path":"/v1/users"}
{"id":2,"method":"GET","path":"/v1/users/{user_id}"}npx @apitally/cli metrics <app-id> --since <datetime> --metrics <json> \
[--until <datetime>] [--interval <interval>] [--group-by <json>] \
[--filters <json>] [--timezone <tz>] [--db [<path>]]
Fetch aggregated metrics for an app. Outputs NDJSON to stdout by default.
--since: Start of time range, inclusive (ISO 8601 or relative duration, required)--until: End of time range, exclusive (ISO 8601 or relative duration, defaults to now)--metrics: Comma-separated list or JSON array of metric names to include (required)--interval: Time interval for grouping (month,day,hour,minute). When omitted, returns a single row per group for the entire time range--group-by: Comma-separated list or JSON array of field names to group by, in addition to time period--filters: JSON array of filter objects (see below)--timezone: Timezone for intervals and to interpret since/until if not tz-aware (defaults to system timezone)--db: Write tometricstable in DuckDB instead of outputting NDJSON to stdout
Deduplication in DuckDB: Deletes all existing rows for the same app_id within the fetched time range before inserting new data.
| Metric | Type | Description |
|---|---|---|
requests |
integer | Total request count |
requests_per_minute |
float | Requests per minute |
bytes_received |
integer | Total bytes received |
bytes_sent |
integer | Total bytes sent |
client_errors |
integer | 4xx errors (excluding expected errors) |
server_errors |
integer | 5xx errors (excluding expected errors) |
error_rate |
float | Ratio of errors to total requests |
response_time_p50 |
integer | 50th percentile response time (ms) |
response_time_p75 |
integer | 75th percentile response time (ms) |
response_time_p90 |
integer | 90th percentile response time (ms) |
response_time_p95 |
integer | 95th percentile response time (ms) |
response_time_p99 |
integer | 99th percentile response time (ms) |
env, consumer_id, method, path, status_code
Pass --filters as a JSON array of filter objects. Supported fields and operators:
- string fields (
env,method,path):eq,neq,in,not_in,like,not_like,contains,not_contains - numeric fields (
consumer_id,status_code):eq,neq,gt,gte,lt,lte,in,not_in,is_null,is_not_null
Filter examples:
[{"field":"method","op":"eq","value":"GET"}]
[{"field":"status_code","op":"gte","value":400}]
[{"field":"path","op":"like","value":"/v1/users/%"}]Example NDJSON output (without --db):
{"period_start":"2026-01-01T00:00:00Z","period_end":"2026-01-01T01:00:00Z","env":"prod","requests":1234,"error_rate":0.02}
{"period_start":"2026-01-01T01:00:00Z","period_end":"2026-01-01T02:00:00Z","env":"prod","requests":987,"error_rate":0.01}npx @apitally/cli request-logs <app-id> --since <datetime> \
[--until <datetime>] [--fields <json>] [--filters <json>] \
[--sample <n|rate>] [--limit <n>] [--db [<path>]]
Fetch request log data for an app. Outputs NDJSON to stdout by default.
--since: Start of time range, inclusive (ISO 8601 or relative duration, required)--until: End of time range, exclusive (ISO 8601 or relative duration, defaults to now)--fields: Comma-separated list or JSON array of field names to include--filters: JSON array of filter objects--sample: Approximate sample size (integer, e.g.1000) or sample rate (float > 0 and <= 0.5, e.g.0.1for ~10%)--limit: Maximum number of rows (hard cap: 1,000,000)--db: Write torequest_logstable in DuckDB instead of outputting NDJSON to stdout
Timestamps without timezone are treated as UTC. Results are ordered by timestamp ascending.
| Field | Type | Default |
|---|---|---|
timestamp |
string (datetime) | yes |
request_uuid |
string (ID) | yes |
env |
string | yes |
method |
string | yes |
path |
string | yes |
url |
string | yes |
consumer_id |
int (ID) | yes |
request_headers |
array of string tuples (headers) | no |
request_size_bytes |
int | yes |
request_body_json |
string (JSON) | no |
status_code |
int | yes |
response_time_ms |
int | yes |
response_headers |
array of string tuples (headers) | no |
response_size_bytes |
int | yes |
response_body_json |
string (JSON) | no |
client_ip |
string | yes |
client_country_iso_code |
string | yes |
exception_type |
string | no |
exception_message |
string | no |
exception_stacktrace |
string | no |
sentry_event_id |
string (ID) | no |
trace_id |
string (ID) | no |
Default fields are included when --fields is omitted. When --fields is provided, it replaces the default set and only the specified fields are returned. timestamp, request_uuid, method, and url are always included regardless.
Pass --filters as a JSON array of filter objects. Multiple filters are combined with AND.
Filter object keys:
field: field name to filter onop: comparison operatorvalue: comparison value (omit forexists/not_exists)key: header name, required only forrequest_headersandresponse_headers
All fields can be used in filters. Available operators depend on the field type:
- string / string (JSON):
eq,neq,in,not_in,like,not_like,ilike,not_ilike,contains,not_contains,is_null,is_not_null - string (datetime):
eq,neq,gt,gte,lt,lte— value is an ISO 8601 datetime string - string (ID) / int (ID):
eq,neq,in,not_in,is_null,is_not_null - array of string tuples (headers):
eq,neq,in,not_in,like,not_like,ilike,not_ilike,contains,not_contains,exists,not_exists— requireskey - int:
eq,neq,gt,gte,lt,lte,in,not_in
in/not_in: value must be a JSON array (of strings or ints matching the field type)exists/not_exists/is_null/is_not_null: omit value entirelylike/ilike/not_like/not_ilike: use%as wildcardcontains/not_contains: case-insensitive substring match (no wildcards needed)
[{"field": "consumer_id", "op": "eq", "value": 42}]
[{"field": "consumer_id", "op": "is_null"}]
[{"field": "path", "op": "eq", "value": "/v1/users/{user_id}"}]
[{"field": "url", "op": "ilike", "value": "%/users/123%"}]
[{"field": "status_code", "op": "gte", "value": 400},{"field": "status_code", "op": "lt", "value": 500}]
[{"field": "request_headers", "key": "x-api-version", "op": "exists"}]
[{"field": "request_headers", "key": "content-type", "op": "eq", "value": "application/json"}]
[{"field": "response_body_json", "op": "contains", "value": "error"}]Example NDJSON output (without --db):
{"timestamp":"2026-01-01T00:15:00.000Z","request_uuid":"2fbc1df6-3124-4ed1-a376-7d2c64e4d5cf","env":"prod","method":"GET","path":"/test/1","url":"https://api.example.com/test/1","consumer_id":1,"request_size_bytes":0,"status_code":404,"response_time_ms":122,"response_size_bytes":66,"client_ip":"203.0.113.10","client_country_iso_code":"DE"}
{"timestamp":"2026-01-01T00:16:00.000Z","request_uuid":"c6d32f8a-0bc1-43c1-b6c5-7d04363dc97c","env":"prod","method":"GET","path":"/test/2","url":"https://api.example.com/test/2","consumer_id":1,"request_size_bytes":0,"status_code":500,"response_time_ms":68,"response_size_bytes":66,"client_ip":"198.51.100.22","client_country_iso_code":"US"}npx @apitally/cli request-details <app-id> <request-uuid> [--db [<path>]]
Get full details for a specific request identified by its UUID, including headers, request/response body, exception info, application logs, and spans. Outputs a JSON object to stdout by default.
--db: Write torequest_logs,application_logs, andspanstables in DuckDB instead of outputting JSON to stdout
Example JSON output (without --db):
{"timestamp":"2026-01-01T00:15:00.000Z","request_uuid":"2fbc1df6-3124-4ed1-a376-7d2c64e4d5cf","env":"prod","method":"GET","path":"/test/1","url":"https://api.example.com/test/1","consumer_id":1,"request_headers":[["content-type","application/json"]],"request_size_bytes":0,"request_body_json":null,"status_code":200,"response_time_ms":122,"response_headers":[["x-request-id","abc"]],"response_size_bytes":66,"response_body_json":"{\"ok\":true}","client_ip":"203.0.113.10","client_country_iso_code":"DE","trace_id":"0000000000000000aaaaaaaaaaaaaaaa","exception":null,"logs":[{"timestamp":"2026-01-01T00:15:00.100Z","message":"handling request","level":"INFO","logger":"app","file":"main.py","line":42}],"spans":[{"span_id":"00000000000000aa","parent_span_id":null,"name":"GET /test/1","kind":"SERVER","start_time_ns":1735689600000000000,"end_time_ns":1735689600050000000,"duration_ns":50000000,"status":"OK","attributes":{"http.method":"GET"}}]}npx @apitally/cli sql "<query>" [--db <path>]
npx @apitally/cli sql [--db <path>] < query.sql
echo "<query>" | npx @apitally/cli sql [--db <path>]
Run a SQL query against a local DuckDB database. The query can be passed as an argument or read from stdin. Outputs NDJSON to stdout.
--db: Path to DuckDB database
Available tables: apps, app_envs, consumers, endpoints, metrics, request_logs, application_logs, spans. See duckdb_tables.md for schemas.
Important: The database may contain data from previous sessions. Always filter queries by app_id, time (timestamp for request_logs, period_start/period_end for metrics), and other relevant fields to avoid including unrelated data.
DuckDB uses a PostgreSQL-compatible SQL dialect. The bundled DuckDB has no ICU extension, so TIMESTAMPTZ columns cannot be cast directly to DATE. Use (timestamp AT TIME ZONE 'UTC')::DATE or date_trunc('day', timestamp AT TIME ZONE 'UTC') for date conversion and grouping.
Example output:
{"timestamp":"2026-01-01T00:16:00.000Z","method":"POST","path":"/users","status_code":500}
{"timestamp":"2026-01-01T00:15:00.000Z","method":"GET","path":"/users/{userId}","status_code":404}npx @apitally/cli reset-db [--db <path>]
Drop and recreate all tables in the local DuckDB database. Use this to clear all stored data and start fresh.
--db: Path to DuckDB database