Skip to main content
This guide covers migrating from the legacy v1 analytics system to the powerful v2 SQL-based analytics system.

Overview

The v2 analytics system represents a complete redesign of how you query your verification data, moving from predefined parameters to full SQL flexibility.

Key Changes in v2:

  • Flexibility: Predefined aggregations → Full SQL with custom queries
  • Response Format: Direct array → {meta, data} envelope

Migration Impact:

  • Existing in v1: Predefined analytics with limited filtering options
  • Enhanced in v2: Complete SQL flexibility, custom aggregations, and powerful filtering

Access and Authentication

V1: Automatic Access

V1 analytics was automatically available with api.*.read_api permission:
v1 Analytics Access
curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications?apiId=api_123&start=1620000000000" \
  -H "Authorization: Bearer <your-root-key>"

V2: Opt-in Access Required

V2 analytics requires explicit opt-in and dedicated permissions:
Analytics is currently in private beta and requires manual opt-in.To get access:
  1. Find your workspace ID in dashboard settings
  2. Email support@unkey.dev with:
    • Your workspace ID
    • Your use case (billing, dashboards, reporting, etc.)
    • Expected query volume
Comprehensive Analytics Documentation AvailableThis migration guide covers the technical differences between v1 and v2 analytics. For complete documentation on v2 analytics capabilities, including:
  • Detailed schema reference with all tables and columns
  • 30+ query examples for common use cases
  • Performance optimization tips
  • Query restrictions and error handling
See the Analytics Documentation section, starting with Getting Started.
v2 Example
curl -X POST https://api.unkey.com/v2/analytics.getVerifications \
  -H "Authorization: Bearer <your-root-key-with-analytics-permission>" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT SUM(count) FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY"
  }'

Permission Changes

V1 PermissionV2 PermissionDescription
api.*.read_apiapi.*.read_analyticsAccess analytics for all APIs
api.api_123.read_apiapi.api_123.read_analyticsAccess analytics for specific API
We automatically filter queries based on your permissions. If you have api.api_123.read_analytics, your queries will only return data from that API. Wildcard api.*.read_analytics allows access to all APIs in your workspace and you can filter by apiId in your query.

Request Format Changes

V1: Query Parameters

V1 used predefined query parameters with limited flexibility:
v1 Request Structure
curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications" \
  -H "Authorization: Bearer <root-key>" \
  -G \
  -d "apiId=api_123" \
  -d "start=1620000000000" \
  -d "end=1622592000000" \
  -d "groupBy=day" \
  -d "externalId=user_456" \
  -d "limit=100"

V2: SQL Queries

V2 uses full SQL queries for complete flexibility with automatic security filtering:
v2 Request Structure
curl -X POST https://api.unkey.com/v2/analytics.getVerifications \
  -H "Authorization: Bearer <root-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE key_space_id = '\''ks_123'\'' AND external_id = '\''user_456'\'' AND time >= now() - INTERVAL 30 DAY GROUP BY time ORDER BY time LIMIT 100"
  }'
Automatic Security Filtering: V2 automatically applies security filters based on your root key permissions. You don’t need to manually filter by workspace_id - the system handles this for you.

Response Format Changes

V1 Response Format

V1 returned a direct array with predefined outcome fields:
v1 Response Structure
[
  {
    "time": 1620000000000,
    "valid": 1234,
    "notFound": 56,
    "forbidden": 12,
    "usageExceeded": 8,
    "rateLimited": 234,
    "unauthorized": 45,
    "disabled": 3,
    "insufficientPermissions": 67,
    "expired": 89,
    "total": 1548
  },
  {
    "time": 1620086400000,
    "valid": 1456,
    "notFound": 67,
    // ... more outcome fields
    "total": 1678
  }
]

V2 Response Format

V2 uses standard envelope format with your custom query results:
v2 Response Structure
{
  "meta": {
    "requestId": "req_analytics789"
  },
  "data": [
    {
      "time": "2024-01-01",
      "total": 1548
    },
    {
      "time": "2024-01-02",
      "total": 1678
    }
  ]
}
Dynamic Response Structure: The data array contains objects with fields determined by your SQL SELECT clause. Unlike v1’s fixed structure, v2 returns exactly what you query for.

Common Migration Patterns

Basic Usage Count

V1: Basic Query

curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications?apiId=api_123&start=1620000000000&groupBy=day"

V2: SQL Equivalent

curl -X POST https://api.unkey.com/v2/analytics.getVerifications \
  -H "Authorization: Bearer <root-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE key_space_id = '\''ks_123'\'' AND time >= now() - INTERVAL 30 DAY GROUP BY time ORDER BY time"
  }'

Filter by User

V1: External ID Filter

curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications?apiId=api_123&externalId=user_456&groupBy=day"

V2: SQL Filter

curl -X POST https://api.unkey.com/v2/analytics.getVerifications \
  -H "Authorization: Bearer <root-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE key_space_id = '\''ks_123'\'' AND external_id = '\''user_456'\'' AND time >= now() - INTERVAL 30 DAY GROUP BY time ORDER BY time"
  }'

Outcome Breakdown

V1: Automatic Outcome Fields

curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications?apiId=api_123&groupBy=day"

V2: Custom Outcome Aggregation

curl -X POST https://api.unkey.com/v2/analytics.getVerifications \
  -H "Authorization: Bearer <root-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT time, sumIf(count, outcome = '\''VALID'\'') AS valid, sumIf(count, outcome = '\''RATE_LIMITED'\'') AS rateLimited, sumIf(count, outcome = '\''INVALID'\'') AS invalid, SUM(count) AS total FROM key_verifications_per_day_v1 WHERE key_space_id = '\''ks_123'\'' AND time >= now() - INTERVAL 30 DAY GROUP BY time ORDER BY time"
  }'

Top Users by Usage

V1: Group by Identity

curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications?apiId=api_123&groupBy=identity&orderBy=total&order=desc&limit=10"

V2: SQL with Ranking

curl -X POST https://api.unkey.com/v2/analytics.getVerifications \
  -H "Authorization: Bearer <root-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "SELECT external_id, SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE key_space_id = '\''ks_123'\'' AND time >= now() - INTERVAL 30 DAY AND external_id != '\'''\'' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 10"
  }'

Getting Help