Skip to main content

Schema Reference

Complete reference of tables, columns, and data types available in Unkey Analytics. Explore the verification and key event data schemas.
6 min read

Unkey Analytics stores verification events across multiple time-series tables for efficient querying. This reference documents all available tables and their columns.

Tip

Use aggregated tables (per_hour, per_day, per_month) for queries spanning long time periods to improve performance.

Raw Events Table#

The key_verifications_v1 table contains individual verification events as they occur.

Columns#

ColumnTypeDescription
request_idStringUnique identifier for each verification request
timeInt64Unix timestamp in milliseconds when verification occurred
workspace_idStringWorkspace identifier (automatically filtered - you don't need to filter by this)
key_space_idStringYour KeySpace identifier (e.g., ks_1234). Automatically filtered if your root key is scoped to a single keyspace, otherwise filter this yourself.
external_idStringYour user's identifier (e.g., user_abc) - use this to filter by user
key_idStringIndividual API key identifier
outcomeStringVerification result (see Outcome Values)
regionStringUnkey region that handled the verification
sourceStringVerification origin. api means the Verify API, and gateway means a gateway API key policy
app_idStringApp whose gateway policy verified the key. Empty when source is not gateway
tagsArray(String)Custom tags added during verification
spent_creditsInt64Number of credits spent on this verification (0 if no credits were spent)

Outcome Values#

The outcome column contains one of these values:

OutcomeDescription
VALIDKey is valid and verification succeeded
RATE_LIMITEDVerification exceeded rate limit
INVALIDKey not found or malformed
EXPIREDKey has expired
DISABLEDKey is disabled
INSUFFICIENT_PERMISSIONSKey lacks required permissions
FORBIDDENOperation not allowed for this key
USAGE_EXCEEDEDKey has exceeded usage limit

Aggregated Tables#

Pre-aggregated tables provide better query performance for long time ranges. Each aggregated table includes outcome counts.

Per Minute Table#

key_verifications_per_minute_v1 - Aggregated by minute

ColumnTypeDescription
timeDateTime/DateDateTime for minute/hour tables, Date for day/month tables
workspace_idStringWorkspace identifier (automatically filtered - you don't need to filter by this)
key_space_idStringKeySpace identifier. Automatically filtered if your root key is scoped to a single keyspace, otherwise filter this yourself.
external_idStringYour user identifier
key_idStringAPI key identifier
outcomeStringVerification outcome (VALID, RATE_LIMITED, INVALID, etc.)
sourceStringVerification origin (api or gateway)
app_idStringApp whose gateway policy verified the key. Empty for non-gateway verifications
tagsArrayTags associated with verifications
countUInt64Total verification count for this aggregation
spent_creditsUInt64Total credits spent

Per Hour Table#

key_verifications_per_hour_v1 - Aggregated by hour. Same columns as per-minute table.

Per Day Table#

key_verifications_per_day_v1 - Aggregated by day. Same columns as per-minute table.

Per Month Table#

key_verifications_per_month_v1 - Aggregated by month. Same columns as per-minute table.

Filtering by keyspace and user#

You can use your familiar identifiers directly in queries:

  • key_space_id - Your KeySpace ID (e.g., ks_1234). Find this in your Keyspace Settings.
  • external_id - Your user identifiers (e.g., user_abc123) from your application

All standard comparison operators are supported: =, !=, <, >, <=, >=, IN, NOT IN

Filter by keyspace#

Filter by User#

Multiple Values#

Filter gateway verifications by app#

Verifications from the gateway use source = 'gateway' and include the app ID that ran the API key policy. Verifications through the Verify API use source = 'api' and an empty app_id.

Working with Tags#

Tags are stored as Array(String) and require array functions to query.

Check if tag exists#

Check if any tag exists#

Check if all tags exist#

Extract and group by tags#

Filter tags with pattern#

Time Functions#

Timestamps are stored differently depending on the table:

  • Raw table (key_verifications_v1): time is Int64 (Unix milliseconds)
  • Aggregated tables: time is DateTime

Current Time#

Time Ranges (Raw Table)#

For the raw key_verifications_v1 table, compare time with millisecond timestamps:

Time Ranges (Aggregated Tables)#

For aggregated tables, use DateTime comparisons directly:

Time Rounding (Raw Table)#

Specific Date Ranges#

Common ClickHouse Functions#

Aggregate Functions#

FunctionDescriptionExample
COUNT()Count rowsSELECT COUNT(*) FROM key_verifications_v1
SUM()Sum valuesSELECT SUM(valid_count) FROM key_verifications_per_day_v1
AVG()AverageSELECT AVG(spent_credits) FROM key_verifications_v1
MIN()Minimum valueSELECT MIN(time) FROM key_verifications_v1
MAX()Maximum valueSELECT MAX(time) FROM key_verifications_v1
countIf()Conditional countSELECT countIf(outcome = 'VALID')
uniq()Count distinctSELECT uniq(key_id) FROM key_verifications_v1

String Functions#

FunctionDescriptionExample
lower()Convert to lowercaseWHERE lower(outcome) = 'valid'
upper()Convert to uppercaseWHERE upper(region) = 'US-EAST-1'
concat()Concatenate stringsSELECT concat(region, '-', outcome)
substring()Extract substringSELECT substring(key_id, 1, 8)
startsWith()Check prefixWHERE startsWith(key_id, 'key_')

Array Functions#

FunctionDescriptionExample
has()Check elementWHERE has(tags, 'environment=production')
hasAny()Check any elementWHERE hasAny(tags, ['team=backend', 'team=api'])
hasAll()Check all elementsWHERE hasAll(tags, ['environment=prod', 'tier=1'])
arrayJoin()Expand arraySELECT arrayJoin(tags) as tag
arrayFilter()Filter arrayarrayFilter(x -> startsWith(x, 'path='), tags)
length()Array lengthWHERE length(tags) > 0

Math Functions#

FunctionDescriptionExample
round()Round numberSELECT round(AVG(spent_credits), 2)
floor()Round downSELECT floor(spent_credits / 100) * 100 as credit_bucket
ceil()Round upSELECT ceil(spent_credits)
abs()Absolute valueSELECT abs(difference)

Conditional Functions#

FunctionDescriptionExample
if()If-then-elseSELECT if(outcome = 'VALID', 1, 0)
CASEMulti-conditionCASE WHEN outcome = 'VALID' THEN 'success' ELSE 'failure' END

Performance Tips#

  1. Always filter by time - Use time-based WHERE clauses to leverage indexes
  2. Use aggregated tables - Query hourly/daily/month tables for long ranges
  3. Limit result sets - Add LIMIT clauses to prevent large results
  4. Filter before grouping - Use WHERE instead of HAVING when possible
  5. Avoid SELECT * - Only select columns you need

Query Limits#

ResourceLimitError Code
Execution time30 secondsquery_execution_timeout
Memory usage1 GBquery_memory_limit_exceeded
Rows to read10 millionquery_rows_limit_exceeded
Queries per hour1000query_quota_exceeded

See Query Restrictions for more details on query limits and restrictions.