# Using Cursor with Unkey Source: https://unkey.com/docs/ai-code-gen/cursor Leverage Cursor's AI capabilities to build applications with Unkey's APIs Cursor is an AI-powered code editor that can help you build applications faster. When combined with Unkey's APIs, you can quickly generate secure, scalable applications with API key management and rate limiting. ## Prerequisites ## Getting Started ### 1. Set Up Your Unkey Workspace First, create your Unkey workspace and get your API keys: Navigate to the [Unkey Dashboard](https://app.unkey.com/apis) and create a new API for your project. Go to [Settings > Root Keys](https://app.unkey.com/settings/root-keys) and create a new root key with the necessary permissions. Copy your API ID from the dashboard - you'll need this for generating API keys. ### 2. Set Up Unkey MCP Server (Optional) Cursor supports the Model Context Protocol (MCP) which allows you to connect directly to Unkey's APIs. This gives Cursor access to your Unkey workspace for more intelligent suggestions. #### Install Unkey MCP Server 1. **Configure the MCP Server** Create or update your Cursor configuration file with the Unkey MCP server: ```json theme={null} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_ROOT_KEY}" ] } } } ``` For ratelimiting specific operations, you can also add: ```json theme={null} { "mcpServers": { "UnkeyRateLimiting": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/ratelimits/", "--header", "MCP-UNKEY-V2-ROOT-KEY:${UNKEY_ROOT_KEY}" ] } } } ``` 2. **Set Environment Variable** ```bash theme={null} export UNKEY_ROOT_KEY="your_root_key_here" ``` 3. **Restart Cursor** Restart Cursor to load the MCP server configuration. ## Tips and Tricks for Cursor with Unkey ### 1. Keep Your Requests Small When working with Cursor, break down complex tasks into smaller, focused requests: **Good:** ``` Create a function to verify an API key with Unkey that returns a boolean ``` **Better:** ``` Create a TypeScript function that: - Takes an API key string as input - Uses @unkey/api to verify the key - Returns a boolean indicating if the key is valid - Includes proper error handling ``` ### 2. Update and Reference Your README.md Keep your project's README.md updated with Unkey-specific information. Cursor uses this context to provide better suggestions: ```markdown theme={null} # My Project This project uses Unkey for API authentication and ratelimiting. ## Environment Variables - `UNKEY_ROOT_KEY`: Your Unkey root key - `UNKEY_API_ID`: Your API ID from the Unkey dashboard ## API Routes - `/api/protected` - Requires valid API key - `/api/keys` - Manage API keys (admin only) ## Rate Limiting - Free tier: 100 requests/hour - Pro tier: 1000 requests/hour ``` ## Add Unkey Documentation Context Adding Unkey docs can let you specifically refer to Unkey features when building your app. From Cursor Settings > Features > Docs add new doc, use the URL "[https://unkey.com/docs](https://unkey.com/docs)" # AI Code Gen with Unkey Source: https://unkey.com/docs/ai-code-gen/overview Use AI-powered code generation tools with Unkey's APIs and services Unkey provides powerful integrations with AI code generation tools to help you build applications faster and more efficiently. Whether you're using Cursor, Windsurf, or other AI tools, you can leverage AI to generate code that integrates seamlessly with Unkey's APIs. ## Available AI Tools Use Cursor's AI capabilities with Unkey's API documentation and examples Build Unkey applications with Windsurf's AI-powered development environment Connect your favorite AI code generation tool to Unkey's APIs using Model Context Protocol # Unkey MCP (Model Context Protocol) Source: https://unkey.com/docs/ai-code-gen/unkey-mcp Connect AI tools to Unkey's APIs using Model Context Protocol The Unkey Model Context Protocol (MCP) servers provide direct integration between AI tools and Unkey's APIs. This allows you to interact with your Unkey workspace directly, enabling AI-powered API key management, rate limiting configuration, and analytics queries. ## What is MCP? Model Context Protocol (MCP) is an open standard that allows AI applications to securely access external data and services. Unkey's MCP servers give Claude Desktop direct access to your Unkey APIs, enabling intelligent assistance with API key management and rate limiting. ## Available MCP Servers Unkey provides two MCP servers: Full access to Unkey's API management capabilities Specialized server for rate limiting operations ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) created * Unkey root key with appropriate permissions * Node.js installed (for npx command) ## Installation Below is an example of using Unkey MCP with Claude but it can also be used with other AI applications. ### Unkey API MCP The main Unkey MCP server provides access to the complete Unkey API: 1. **Open Claude Desktop Configuration** Navigate to your Claude Desktop configuration file: * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 2. **Add the MCP Server Configuration** Add the following configuration to your `claude_desktop_config.json`: ```json theme={null} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_ROOT_KEY}" ] } } } ``` 3. **Set Environment Variable** Set your Unkey root key as an environment variable: ```bash theme={null} # macOS/Linux export UNKEY_ROOT_KEY="unkey_xxx" # Windows set UNKEY_ROOT_KEY=unkey_xxx ``` 4. **Restart Claude Desktop** Close and restart Claude Desktop to load the MCP server. ### Unkey Ratelimiting MCP For specialized rate limiting operations, use the dedicated ratelimiting MCP server: 1. **Add Ratelimiting MCP Configuration** Add this configuration to your `claude_desktop_config.json`: ```json theme={null} { "mcpServers": { "UnkeyRateLimiting": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/ratelimits/", "--header", "MCP-UNKEY-V2-ROOT-KEY:${UNKEY_ROOT_KEY}" ] } } } ``` 2. **Use Both Servers** You can configure both MCP servers simultaneously: ```json theme={null} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_ROOT_KEY}" ] }, "UnkeyRateLimiting": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/ratelimits/", "--header", "MCP-UNKEY-V2-ROOT-KEY:${UNKEY_ROOT_KEY}" ] } } } ``` ## Getting Your Root Key To use the MCP servers, you need a Unkey root key with appropriate permissions: Go to [Settings > Root Keys](https://app.unkey.com/settings/root-keys) in your Unkey dashboard. Click "Create New Root Key" and select the permissions you need: * **API Key Management**: For creating, updating, and deleting API keys * **Rate Limiting**: For configuring rate limits * **Analytics**: For querying usage data Copy your root key and store it securely. You'll use this as your environment variable. ## Using MCP with Claude Desktop Once configured, you can interact with your Unkey workspace directly through Claude Desktop: ### API Key Management Ask Claude to help with API key operations: ``` Can you create a new API key for my mobile app with the following settings: - Name: "Mobile App Production" - Rate limit: 1000 requests per hour - Expiration: 90 days from now - Metadata: {"app": "mobile", "version": "1.0", "environment": "production"} ``` ### Ratelimiting Configuration Configure rate limits through natural language: ``` I need to set up rate limiting for my API with these tiers: - Free tier: 100 requests per hour - Pro tier: 1000 requests per hour - Enterprise tier: 10000 requests per hour Can you help me configure these limits? ``` ### Analytics and Monitoring Query your API usage data: ``` Show me the API usage statistics for the last 7 days, including: - Total requests - Top 5 API keys by usage - Error rates - Geographic distribution of requests ``` ### Troubleshooting Get help with common issues: ``` I'm seeing 401 errors for API key verification. Can you help me debug this? The API key is: uk_xxx The API ID is: api_xxx ``` ## Available Commands ### API Key Operations * **Create API Key**: Generate new API keys with custom settings * **List API Keys**: View all API keys in your workspace * **Update API Key**: Modify existing API key properties * **Delete API Key**: Remove API keys from your workspace * **Verify API Key**: Check if an API key is valid and active ### Rate Limiting Operations * **Configure Rate Limits**: Set up rate limiting rules * **Check Rate Limit Status**: Monitor current rate limit usage * **Update Rate Limits**: Modify existing rate limit configurations * **Delete Rate Limits**: Remove rate limiting rules ### Analytics and Monitoring * **Usage Analytics**: Query API usage statistics * **Error Analysis**: Investigate API errors and issues * **Performance Metrics**: Monitor API performance data * **Usage Reports**: Generate custom usage reports ## Example Conversations ### Creating an API Key **You**: "Create a new API key for my development environment with a rate limit of 500 requests per hour and set it to expire in 30 days." **Claude**: "I'll create a new API key for your development environment with the specified settings. Let me use the Unkey MCP to create this key with a 500 requests/hour rate limit and 30-day expiration." ### Analyzing API Usage **You**: "What are my top 3 API keys by usage this month?" **Claude**: "Let me query your Unkey analytics to find your top 3 API keys by usage this month. I'll pull the usage data and provide you with detailed statistics." ### Debugging Issues **You**: "I'm getting rate limit errors but I thought my limit was higher. Can you check my current rate limit configuration?" **Claude**: "I'll check your current rate limit configuration using the Unkey MCP. Let me examine your rate limiting settings and current usage to help diagnose the issue." ## Security Best Practices ### Environment Variables * Store your root key in environment variables, never in configuration files * Use different root keys for different environments (development, staging, production) * Regularly rotate your root keys ### Permissions * Grant only the minimum required permissions to your root keys * Use separate root keys for different operations when possible * Monitor root key usage through audit logs ### Access Control * Limit access to your Claude Desktop configuration * Use secure storage for your root keys * Implement proper backup and recovery procedures ## Troubleshooting ### Common Issues 1. **MCP Server Not Loading** * Check that Node.js is installed and accessible * Verify your configuration file syntax * Ensure environment variables are set correctly 2. **Authentication Errors** * Verify your root key is correct and has proper permissions * Check that the environment variable is set * Confirm your root key hasn't expired 3. **Connection Issues** * Ensure you have internet connectivity * Check if your firewall is blocking connections * Verify the MCP server URLs are correct ### Getting Help If you encounter issues: 1. Check the Claude Desktop logs for error messages 2. Verify your configuration matches the examples exactly 3. Test your root key directly with the Unkey API 4. Join the [Unkey Discord](https://go.unkey.com/discord) for community support ## Advanced Configuration ### Custom Environment Variables You can use custom environment variable names: ```json theme={null} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${MY_CUSTOM_UNKEY_KEY}" ] } } } ``` ### Multiple Workspaces Configure multiple Unkey workspaces: ```json theme={null} { "mcpServers": { "UnkeyProduction": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_PROD_KEY}" ] }, "UnkeyStaging": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_STAGING_KEY}" ] } } } ``` ## Next Steps * Explore [Cursor with Unkey](/ai-code-gen/cursor) for IDE-based AI assistance * Check out [Windsurf with Unkey](/ai-code-gen/windsurf) for collaborative development ## Resources * [Unkey API Reference](/api-reference/v2/auth) * [Unkey Dashboard](https://app.unkey.com) * [Community Discord](https://go.unkey.com/discord) # Using Windsurf with Unkey Source: https://unkey.com/docs/ai-code-gen/windsurf Build applications with Windsurf's AI-powered development environment and Unkey's APIs Windsurf is an AI-powered development environment that combines the power of AI assistance with a full-featured IDE. When integrated with Unkey's APIs, you can rapidly build secure, scalable applications with intelligent code generation and real-time collaboration features. ## Getting Started ### 1. Set Up Your Unkey Environment Before working with Windsurf, ensure you have your Unkey credentials ready: Navigate to the [Unkey Dashboard](https://app.unkey.com/apis) and create a new API for your project. Go to [Settings > Root Keys](https://app.unkey.com/settings/root-keys) and create a new root key with appropriate permissions. Note down your API ID and root key - you'll need these for your application. ### 2. Set Up Unkey MCP Server (Optional) Windsurf supports the Model Context Protocol (MCP) which allows you to connect directly to Unkey's APIs. This gives Windsurf access to your Unkey workspace for more intelligent suggestions. #### Install Unkey MCP Server To get started with Windsurf, open "Windsurf Settings > Cascade > Model Context Protocol (MCP) Servers", click on "Add Server", click "Add custom server", and add the following configuration for Unkey. 1. **Configure the MCP Server** ```json theme={null} { "mcpServers": { "Unkey": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/v2", "--header", "MCP-UNKEY-BEARER-AUTH:${UNKEY_ROOT_KEY}" ] } } } ``` For ratelimiting specific operations, you can also add: ```json theme={null} { "mcpServers": { "UnkeyRateLimiting": { "command": "npx", "args": [ "mcp-remote", "https://mcp.unkey.com/mcp/ratelimits/", "--header", "MCP-UNKEY-V2-ROOT-KEY:${UNKEY_ROOT_KEY}" ] } } } ``` 2. **Set Environment Variable** ```bash theme={null} export UNKEY_ROOT_KEY="your_root_key_here" ``` 3. **Restart Windsurf** Restart Windsurf to load the MCP server configuration. # Getting Started Source: https://unkey.com/docs/analytics/getting-started Request access and run your first analytics query ## Request Access **Analytics is currently in private beta and available by request only.** To get started: 1. **Find your workspace ID** in the Unkey dashboard settings 2. **Email us** at [support@unkey.com](mailto:support@unkey.com) with: * Your workspace ID * Your use case (billing, dashboards, reporting, etc.) * Expected query volume We'll enable analytics for your workspace and send you confirmation. ## Authentication Analytics queries require a root key with analytics permissions. Create one in your dashboard: 1. Go to **Settings** → **Root Keys** 2. Click **Create New Root Key** 3. Select permissions: `api.*.read_analytics` OR `api..read_analytics` 4. Copy and securely store your root key Root keys have powerful permissions. Store them securely and never commit them to version control. ## Your First Query Once you have access, execute your first analytics query using the `/v2/analytics.getVerifications` endpoint. ### Count Total Verifications Count the total number of key verifications in the last 7 days across all your APIs to get a high-level view of your overall usage volume. ```sql SQL theme={null} SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY" }' ``` ### Break Down by Outcome Group verifications by their outcome (`VALID`, `RATE_LIMITED`, `USAGE_EXCEEDED`, etc.) over the last 24 hours to understand the distribution of successful vs. failed requests. ```sql SQL theme={null} SELECT outcome, SUM(count) as count FROM key_verifications_per_hour_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY outcome ORDER BY count DESC ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT outcome, SUM(count) as count FROM key_verifications_per_hour_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY outcome ORDER BY count DESC" }' ``` ### Top Users by Usage Identify your most active users by counting their total verifications over the last 30 days to spot power users or potential abuse patterns. ```sql SQL theme={null} SELECT external_id, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id ORDER BY verifications DESC LIMIT 10 ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id ORDER BY verifications DESC LIMIT 10" }' ``` **Performance tip:** For longer time ranges, use pre-aggregated tables instead of the raw table: * `key_verifications_per_minute_v1` - For queries spanning hours * `key_verifications_per_hour_v1` - For queries spanning days * `key_verifications_per_day_v1` - For queries spanning weeks/months * `key_verifications_per_month_v1` - For queries spanning years Use `SUM(count)` instead of `COUNT(*)` with aggregated tables. They scan far fewer rows and are much faster. Check out the [Query Examples](/analytics/query-examples) page for 30+ ready-to-use queries covering billing, monitoring, and analytics use cases. ## Understanding the Response Analytics queries return data as an array of objects: ```json theme={null} { "meta": { "requestId": "req_xxx" }, "data": [ { "outcome": "VALID", "count": 1234 }, { "outcome": "RATE_LIMITED", "count": 56 }, { "outcome": "USAGE_EXCEEDED", "count": 12 } ] } ``` Each object in the `data` array contains fields from your SELECT clause. The field names match the column names or aliases you specified in your query. ## Filtering by API or User You can filter queries to specific APIs or users. Use `key_space_id` to filter by API (find this identifier in your API settings) and `external_id` to filter by user. These fields support standard SQL operators: `=`, `!=`, `IN`, `NOT IN`, `<`, `>`, etc. **Automatic filtering:** All queries are automatically filtered based on your root key's permissions: * **Workspace filtering:** All queries are scoped to your workspace. You **do not need** to filter by `workspace_id`. * **API filtering:** If your root key has `api..read_analytics` permissions (scoped to a specific API), queries are automatically filtered to that API's `key_space_id`. If your root key has `api.*.read_analytics` (all APIs), you should filter by `key_space_id` yourself to query specific APIs. Queries are subject to resource limits (execution time, memory, result size, and quota). See [Query Restrictions](/analytics/query-restrictions) for complete details on limits and error codes. # Overview Source: https://unkey.com/docs/analytics/overview Query your verification data with SQL **Analytics is currently in private beta and available by request only.** See [Getting Started](/analytics/getting-started) for access instructions. ## What is Unkey Analytics? Unkey Analytics provides a powerful SQL interface to query your API key verification data. Instead of building your own analytics pipeline, you can leverage Unkey's built-in data warehouse to: * **Build custom dashboards** for internal teams or end-users * **Power usage-based billing** by querying verification counts per user/organization * **Generate reports** on API usage patterns, top users, and performance metrics * **Monitor and alert** on verification outcomes, rate limits, and errors **Automatic filtering:** All queries are scoped to your workspace automatically. If your root key is scoped to a specific API (`api..read_analytics`), queries are also filtered to that API's `key_space_id`. ## How it Works Every key verification request is automatically stored and aggregated across multiple time-series tables: ```mermaid theme={null} graph LR A[Verify Key Request] --> B[Raw Events Table] B --> C[Minute Aggregates] C --> D[Hour Aggregates] D --> E[Day Aggregates] E --> F[Month Aggregates] ``` You can query these tables using standard SQL to: * Aggregate verification counts by time period * Group by API, user, or outcome * Filter by region, tags, or custom criteria * Calculate metrics for billing or monitoring ## Available Data Every verification event contains: | Field | Type | Description | | --------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `request_id` | String | Unique identifier for each request | | `time` | Int64 | Unix millisecond timestamp | | `workspace_id` | String | Your workspace identifier (**automatically filtered** - you don't need to filter by this) | | `key_space_id` | String | Your KeySpace identifier (e.g., `ks_1234`). **Automatically filtered** if your root key is scoped to a single API. | | `external_id` | String | Your user's identifier (e.g., `user_abc`) | | `key_id` | String | Individual key identifier | | `outcome` | String | Verification result: `VALID`, `RATE_LIMITED`, `INVALID`, `EXPIRED`, `DISABLED`, `INSUFFICIENT_PERMISSIONS`, `FORBIDDEN`, `USAGE_EXCEEDED` | | `region` | String | Unkey region that handled the verification | | `tags` | Array(String) | Custom tags added during verification | | `spent_credits` | Int64 | Number of credits spent on this verification (0 if no credits were spent) | ## Use Cases Usage-based billing and credit tracking API health and performance monitoring User behavior and engagement insights # Query Examples Source: https://unkey.com/docs/analytics/query-examples Common SQL patterns for analytics and billing This guide provides SQL query examples for common analytics scenarios covering all the use cases from the legacy API and more. All examples use ClickHouse SQL syntax and work with the `/v2/analytics.getVerifications` endpoint. ## Using Queries in API Requests When making API requests, you need to format the SQL query as a JSON string on a single line. Here's how: ```sql SQL theme={null} SELECT COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY" }' ``` Each example below shows both the readable multi-line SQL and the single-line JSON format you can copy directly into your API requests. ## Usage Analytics **Use this for:** High-level usage metrics, health monitoring, and trend analysis. **Key patterns:** Total counts, outcome breakdowns, time series analysis. ### Total verifications in the last 7 days Count total verifications across all APIs in the last 7 days. ```sql SQL theme={null} SELECT SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY" }' ``` ### Verifications by outcome Break down verifications by outcome to understand success vs failure rates. ```sql SQL theme={null} SELECT outcome, SUM(count) as count FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY outcome ORDER BY count DESC ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT outcome, SUM(count) as count FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY outcome ORDER BY count DESC" }' ``` ### All outcomes in a single row Get all verification outcomes in one row with individual columns for each outcome type. ```sql SQL theme={null} SELECT sumIf(count, outcome = 'VALID') AS valid, sumIf(count, outcome = 'RATE_LIMITED') AS rateLimited, sumIf(count, outcome = 'INVALID') AS invalid, sumIf(count, outcome = 'NOT_FOUND') AS notFound, sumIf(count, outcome = 'FORBIDDEN') AS forbidden, sumIf(count, outcome = 'USAGE_EXCEEDED') AS usageExceeded, sumIf(count, outcome = 'UNAUTHORIZED') AS unauthorized, sumIf(count, outcome = 'DISABLED') AS disabled, sumIf(count, outcome = 'INSUFFICIENT_PERMISSIONS') AS insufficientPermissions, sumIf(count, outcome = 'EXPIRED') AS expired, SUM(count) AS total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT sumIf(count, outcome = '\''VALID'\'') AS valid, sumIf(count, outcome = '\''RATE_LIMITED'\'') AS rateLimited, sumIf(count, outcome = '\''INVALID'\'') AS invalid, sumIf(count, outcome = '\''NOT_FOUND'\'') AS notFound, sumIf(count, outcome = '\''FORBIDDEN'\'') AS forbidden, sumIf(count, outcome = '\''USAGE_EXCEEDED'\'') AS usageExceeded, sumIf(count, outcome = '\''UNAUTHORIZED'\'') AS unauthorized, sumIf(count, outcome = '\''DISABLED'\'') AS disabled, sumIf(count, outcome = '\''INSUFFICIENT_PERMISSIONS'\'') AS insufficientPermissions, sumIf(count, outcome = '\''EXPIRED'\'') AS expired, SUM(count) AS total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY" }' ``` ### All outcomes per key Get outcome breakdown for each API key in a single row per key. ```sql SQL theme={null} SELECT key_id, sumIf(count, outcome = 'VALID') AS valid, sumIf(count, outcome = 'RATE_LIMITED') AS rateLimited, sumIf(count, outcome = 'INVALID') AS invalid, sumIf(count, outcome = 'NOT_FOUND') AS notFound, sumIf(count, outcome = 'FORBIDDEN') AS forbidden, sumIf(count, outcome = 'USAGE_EXCEEDED') AS usageExceeded, sumIf(count, outcome = 'UNAUTHORIZED') AS unauthorized, sumIf(count, outcome = 'DISABLED') AS disabled, sumIf(count, outcome = 'INSUFFICIENT_PERMISSIONS') AS insufficientPermissions, sumIf(count, outcome = 'EXPIRED') AS expired, SUM(count) AS total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ORDER BY total DESC LIMIT 100 ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_id, sumIf(count, outcome = '\''VALID'\'') AS valid, sumIf(count, outcome = '\''RATE_LIMITED'\'') AS rateLimited, sumIf(count, outcome = '\''INVALID'\'') AS invalid, sumIf(count, outcome = '\''NOT_FOUND'\'') AS notFound, sumIf(count, outcome = '\''FORBIDDEN'\'') AS forbidden, sumIf(count, outcome = '\''USAGE_EXCEEDED'\'') AS usageExceeded, sumIf(count, outcome = '\''UNAUTHORIZED'\'') AS unauthorized, sumIf(count, outcome = '\''DISABLED'\'') AS disabled, sumIf(count, outcome = '\''INSUFFICIENT_PERMISSIONS'\'') AS insufficientPermissions, sumIf(count, outcome = '\''EXPIRED'\'') AS expired, SUM(count) AS total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ORDER BY total DESC LIMIT 100" }' ``` ### Daily verification trend Track daily verification patterns over the last 30 days. ```sql SQL theme={null} SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date" }' ``` ### Hourly breakdown for today Analyze hourly verification patterns for today with outcome breakdown. ```sql SQL theme={null} SELECT time as hour, outcome, SUM(count) as verifications FROM key_verifications_per_hour_v1 WHERE time >= toStartOfDay(now()) GROUP BY time, outcome ORDER BY time, outcome ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time as hour, outcome, SUM(count) as verifications FROM key_verifications_per_hour_v1 WHERE time >= toStartOfDay(now()) GROUP BY time, outcome ORDER BY time, outcome" }' ``` ## Usage by User **Use this for:** Understanding user behavior, identifying power users, tracking user activity over time. **Key patterns:** User ranking, activity trends, specific user analysis. ### All users ranked by usage Rank all users by their total verification usage over the last 30 days. ```sql SQL theme={null} SELECT external_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = 'RATE_LIMITED' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 100 ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = '\''RATE_LIMITED'\'' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '\'''\'' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 100" }' ``` ### Usage for a specific user Analyze usage patterns for a specific user over the last 30 days. ```sql SQL theme={null} SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = 'RATE_LIMITED' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE external_id = 'user_123' AND time >= now() - INTERVAL 30 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = '\''RATE_LIMITED'\'' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE external_id = '\''user_123'\'' AND time >= now() - INTERVAL 30 DAY" }' ``` ### Top 10 users by API usage Identify your most active users by verification count. ```sql SQL theme={null} SELECT external_id, SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 10 ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '\'''\'' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 10" }' ``` ### Daily usage per user Track daily verification patterns for each user over 30 days. ```sql SQL theme={null} SELECT external_id, time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id, date ORDER BY external_id, date ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id, date ORDER BY external_id, date" }' ``` ## API Analytics **Use this for:** Comparing API performance, usage across different APIs, API-specific analysis. **Key patterns:** API comparison, success rates, per-API breakdowns. ### Usage per API Compare usage across all APIs to identify most active endpoints. ```sql SQL theme={null} SELECT key_space_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_space_id ORDER BY total_verifications DESC ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_space_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_space_id ORDER BY total_verifications DESC" }' ``` ### Usage for a specific API Analyze detailed usage patterns for a specific API over 30 days. ```sql SQL theme={null} SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = 'RATE_LIMITED' THEN count ELSE 0 END) as rate_limited, SUM(CASE WHEN outcome = 'INVALID' THEN count ELSE 0 END) as invalid FROM key_verifications_per_day_v1 WHERE key_space_id = 'ks_1234' AND time >= now() - INTERVAL 30 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = '\''RATE_LIMITED'\'' THEN count ELSE 0 END) as rate_limited, SUM(CASE WHEN outcome = '\''INVALID'\'' THEN count ELSE 0 END) as invalid FROM key_verifications_per_day_v1 WHERE key_space_id = '\''ks_1234'\'' AND time >= now() - INTERVAL 30 DAY" }' ``` ### Compare multiple APIs Calculate success rates for multiple APIs to compare performance. ```sql SQL theme={null} SELECT key_space_id, SUM(count) as verifications, round(SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) / SUM(count) * 100, 2) as success_rate FROM key_verifications_per_day_v1 WHERE key_space_id IN ('ks_1234', 'ks_5678') AND time >= now() - INTERVAL 7 DAY GROUP BY key_space_id ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_space_id, SUM(count) as verifications, round(SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) / SUM(count) * 100, 2) as success_rate FROM key_verifications_per_day_v1 WHERE key_space_id IN ('\''ks_1234'\'', '\''ks_5678'\'') AND time >= now() - INTERVAL 7 DAY GROUP BY key_space_id" }' ``` ## Key Analytics **Use this for:** Individual API key analysis, identifying problematic keys, key-specific usage patterns. **Key patterns:** Key ranking, error analysis, specific key monitoring. ### Usage per key Identify your most frequently used API keys over the last 30 days. ```sql SQL theme={null} SELECT key_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ORDER BY total_verifications DESC LIMIT 100 ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_id, SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ORDER BY total_verifications DESC LIMIT 100" }' ``` ### Usage for a specific key Analyze detailed usage patterns for a specific API key. ```sql SQL theme={null} SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = 'RATE_LIMITED' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE key_id = 'key_1234' AND time >= now() - INTERVAL 30 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = '\''VALID'\'' THEN count ELSE 0 END) as successful, SUM(CASE WHEN outcome = '\''RATE_LIMITED'\'' THEN count ELSE 0 END) as rate_limited FROM key_verifications_per_day_v1 WHERE key_id = '\''key_1234'\'' AND time >= now() - INTERVAL 30 DAY" }' ``` ### Keys with most errors Find API keys that are generating the most errors. ```sql SQL theme={null} SELECT key_id, SUM(count) as total_errors, groupArray(DISTINCT outcome) as error_types FROM key_verifications_per_day_v1 WHERE outcome != 'VALID' AND time >= now() - INTERVAL 7 DAY GROUP BY key_id ORDER BY total_errors DESC LIMIT 20 ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT key_id, SUM(count) as total_errors, groupArray(DISTINCT outcome) as error_types FROM key_verifications_per_day_v1 WHERE outcome != '\''VALID'\'' AND time >= now() - INTERVAL 7 DAY GROUP BY key_id ORDER BY total_errors DESC LIMIT 20" }' ``` ## Tag-Based Analytics **Use this for:** Custom metadata filtering, endpoint analysis, user segmentation using tags. **Key patterns:** Tag filtering, endpoint breakdowns, custom attribute analysis. Tags allow you to add custom metadata to verification requests for filtering and aggregation. ### Filter by single tag Count verifications for requests with a specific tag. ```sql SQL theme={null} SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE has(tags, 'path=/api/v1/users') AND time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE has(tags, '\''path=/api/v1/users'\'') AND time >= now() - INTERVAL 7 DAY" }' ``` ### Filter by multiple tags (OR) Count verifications matching any of multiple tags. ```sql SQL theme={null} SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAny(tags, ['path=/api/v1/users', 'path=/api/v1/posts']) AND time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAny(tags, ['\''path=/api/v1/users'\'', '\''path=/api/v1/posts'\'']) AND time >= now() - INTERVAL 7 DAY" }' ``` ### Filter by multiple tags (AND) Count verifications matching all specified tags. ```sql SQL theme={null} SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAll(tags, ['environment=production', 'team=backend']) AND time >= now() - INTERVAL 7 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAll(tags, ['\''environment=production'\'', '\''team=backend'\'']) AND time >= now() - INTERVAL 7 DAY" }' ``` ### Group by tag Aggregate verifications by individual tags to see usage patterns. ```sql SQL theme={null} SELECT arrayJoin(tags) as tag, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY tag ORDER BY verifications DESC LIMIT 20 ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT arrayJoin(tags) as tag, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY tag ORDER BY verifications DESC LIMIT 20" }' ``` ### Breakdown by endpoint (using path tag) Analyze request volume by API endpoint over the last 24 hours. This query uses the raw table for detailed tag analysis. For longer time ranges, consider using aggregated tables and pre-filtered tags. ```sql SQL theme={null} SELECT arrayJoin(arrayFilter(x -> startsWith(x, 'path='), tags)) as endpoint, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY endpoint ORDER BY requests DESC ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT arrayJoin(arrayFilter(x -> startsWith(x, '\''path='\''), tags)) as endpoint, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY endpoint ORDER BY requests DESC" }' ``` ## Billing & Usage-Based Pricing **Use this for:** Usage-based billing implementation, credit tracking, user tier calculation. **Key patterns:** Credit aggregation, billing cycles, tier determination, cost analysis. ### Monthly credits per user Calculate monthly credit consumption per user for billing. ```sql SQL theme={null} SELECT external_id, toStartOfMonth(time) as month, SUM(spent_credits) as total_credits FROM key_verifications_per_day_v1 WHERE external_id != '' AND time >= toStartOfMonth(now()) GROUP BY external_id, month ORDER BY total_credits DESC ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, toStartOfMonth(time) as month, SUM(spent_credits) as total_credits FROM key_verifications_per_day_v1 WHERE external_id != '\'''\'' AND time >= toStartOfMonth(now()) GROUP BY external_id, month ORDER BY total_credits DESC" }' ``` ### Current billing period credits Calculate credit usage for a specific billing period. ```sql SQL theme={null} SELECT external_id, SUM(spent_credits) as credits_this_period FROM key_verifications_per_day_v1 WHERE external_id = 'user_123' AND time >= 1704067200000 -- Start of billing period (Unix millis) AND time < 1706745600000 -- End of billing period (Unix millis) GROUP BY external_id ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(spent_credits) as credits_this_period FROM key_verifications_per_day_v1 WHERE external_id = '\''user_123'\'' AND time >= 1704067200000 AND time < 1706745600000 GROUP BY external_id" }' ``` ### Credit-based tier calculation Determine user tiers based on monthly credit consumption. ```sql SQL theme={null} SELECT external_id, SUM(spent_credits) as total_credits, CASE WHEN total_credits <= 1000 THEN 'free' WHEN total_credits <= 10000 THEN 'starter' WHEN total_credits <= 100000 THEN 'pro' ELSE 'enterprise' END as tier FROM key_verifications_per_day_v1 WHERE time >= toStartOfMonth(now()) AND external_id = 'user_123' GROUP BY external_id ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT external_id, SUM(spent_credits) as total_credits, CASE WHEN total_credits <= 1000 THEN '\''free'\'' WHEN total_credits <= 10000 THEN '\''starter'\'' WHEN total_credits <= 100000 THEN '\''pro'\'' ELSE '\''enterprise'\'' END as tier FROM key_verifications_per_day_v1 WHERE time >= toStartOfMonth(now()) AND external_id = '\''user_123'\'' GROUP BY external_id" }' ``` ### Daily credit usage and cost Track daily credit consumption and calculate estimated costs. ```sql SQL theme={null} SELECT time as date, SUM(spent_credits) as credits_used, credits_used * 0.001 as estimated_cost -- $0.001 per credit FROM key_verifications_per_day_v1 WHERE external_id = 'user_123' AND time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time as date, SUM(spent_credits) as credits_used, credits_used * 0.001 as estimated_cost FROM key_verifications_per_day_v1 WHERE external_id = '\''user_123'\'' AND time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date" }' ``` ## Advanced Queries **Use this for:** Complex analytical patterns, cohort analysis, moving averages, advanced insights. **Key patterns:** User retention, trend smoothing, complex joins, window functions. ### Cohort analysis: New vs returning users Perform cohort analysis to understand user retention patterns. ```sql SQL theme={null} WITH first_seen AS ( SELECT external_id, min(time) as first_verification FROM key_verifications_per_day_v1 WHERE external_id != '' GROUP BY external_id ) SELECT toDate(kv.time) as date, SUM(CASE WHEN kv.time = fs.first_verification THEN kv.count ELSE 0 END) as new_users, SUM(CASE WHEN kv.time > fs.first_verification THEN kv.count ELSE 0 END) as returning_users FROM key_verifications_per_day_v1 kv JOIN first_seen fs ON kv.external_id = fs.external_id WHERE kv.time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "WITH first_seen AS ( SELECT external_id, min(time) as first_verification FROM key_verifications_per_day_v1 WHERE external_id != '\'''\'' GROUP BY external_id ) SELECT toDate(kv.time) as date, SUM(CASE WHEN kv.time = fs.first_verification THEN kv.count ELSE 0 END) as new_users, SUM(CASE WHEN kv.time > fs.first_verification THEN kv.count ELSE 0 END) as returning_users FROM key_verifications_per_day_v1 kv JOIN first_seen fs ON kv.external_id = fs.external_id WHERE kv.time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date" }' ``` ### Moving average (7-day) Calculate 7-day moving average to smooth out daily fluctuations. ```sql SQL theme={null} SELECT date, verifications, avg(verifications) OVER ( ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) as moving_avg_7d FROM ( SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 60 DAY GROUP BY date ) ORDER BY date ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT date, verifications, avg(verifications) OVER ( ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) as moving_avg_7d FROM ( SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 60 DAY GROUP BY date ) ORDER BY date" }' ``` ## Using Aggregated Tables For better performance on large time ranges, use pre-aggregated tables: ### Hourly aggregates Query hourly verification counts for the last 7 days. ```sql SQL theme={null} SELECT time, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= toStartOfHour(now() - INTERVAL 7 DAY) GROUP BY time ORDER BY time ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= toStartOfHour(now() - INTERVAL 7 DAY) GROUP BY time ORDER BY time" }' ``` ### Daily aggregates Query daily verification counts for the last 30 days. ```sql SQL theme={null} SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toStartOfDay(now() - INTERVAL 30 DAY) GROUP BY time ORDER BY time ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toStartOfDay(now() - INTERVAL 30 DAY) GROUP BY time ORDER BY time" }' ``` ### Monthly aggregates Query monthly verification counts for the last year. ```sql SQL theme={null} SELECT time, SUM(count) as total FROM key_verifications_per_month_v1 WHERE time >= toStartOfMonth(now() - INTERVAL 12 MONTH) GROUP BY time ORDER BY time ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_month_v1 WHERE time >= toStartOfMonth(now() - INTERVAL 12 MONTH) GROUP BY time ORDER BY time" }' ``` ## Filling Gaps in Time Series (WITH FILL) When querying time series data, you may have periods with no activity that result in missing time points. ClickHouse's `WITH FILL` clause ensures all time periods are included in results, filling gaps with zeros. `WITH FILL` is particularly useful for creating charts and visualizations where you need consistent time intervals, even when there's no data for some periods. `WITH FILL` only works when grouping by the time column alone. To include outcome breakdowns or other dimensions, use `sumIf()` to pivot them into columns (see the last example below). **Type matching:** The `time` column type varies by table: * **Hourly/Minute tables**: `DateTime` - use `toStartOfHour(now() - INTERVAL N HOUR)` * **Daily/Monthly tables**: `Date` - use `toDate(now() - INTERVAL N DAY)` or `toDate(toStartOfMonth(...))` WITH FILL expressions must match the column type exactly. ### Hourly data with gaps filled Get hourly verification counts for the last 7 days, including hours with zero activity. ```sql SQL theme={null} SELECT time, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= toStartOfHour(now() - INTERVAL 7 DAY) AND time <= toStartOfHour(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toStartOfHour(now() - INTERVAL 7 DAY) TO toStartOfHour(now()) STEP INTERVAL 1 HOUR ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= toStartOfHour(now() - INTERVAL 7 DAY) AND time <= toStartOfHour(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toStartOfHour(now() - INTERVAL 7 DAY) TO toStartOfHour(now()) STEP INTERVAL 1 HOUR" }' ``` ### Daily data with gaps filled Get daily verification counts for the last 30 days, ensuring all days are present. ```sql SQL theme={null} SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY" }' ``` ### Monthly data with gaps filled Get monthly verification counts for the last 12 months with all months included. ```sql SQL theme={null} SELECT time, SUM(count) as total FROM key_verifications_per_month_v1 WHERE time >= toDate(toStartOfMonth(now() - INTERVAL 12 MONTH)) AND time <= toDate(toStartOfMonth(now())) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(toStartOfMonth(now() - INTERVAL 12 MONTH)) TO toDate(toStartOfMonth(now())) STEP INTERVAL 1 MONTH ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, SUM(count) as total FROM key_verifications_per_month_v1 WHERE time >= toDate(toStartOfMonth(now() - INTERVAL 12 MONTH)) AND time <= toDate(toStartOfMonth(now())) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(toStartOfMonth(now() - INTERVAL 12 MONTH)) TO toDate(toStartOfMonth(now())) STEP INTERVAL 1 MONTH" }' ``` ### Filling gaps with aggregations For more complex queries that aggregate by outcome, use a subquery or pivot approach instead of WITH FILL with multiple GROUP BY columns. ```sql SQL theme={null} -- Pivot outcomes into columns with all days filled SELECT time, sumIf(count, outcome = 'VALID') as valid, sumIf(count, outcome = 'RATE_LIMITED') as rate_limited, sumIf(count, outcome = 'INVALID') as invalid, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "query": "SELECT time, sumIf(count, outcome = '\''VALID'\'') as valid, sumIf(count, outcome = '\''RATE_LIMITED'\'') as rate_limited, sumIf(count, outcome = '\''INVALID'\'') as invalid, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY" }' ``` `WITH FILL` only works when grouping by time alone. For outcome breakdowns, use `sumIf()` to pivot outcomes into separate columns as shown above. ## Tips for Efficient Queries 1. **Always filter by time** - Use indexes by including time filters 2. **Use aggregated tables** - Hourly/daily/monthly tables for longer ranges 3. **Add LIMIT clauses** - Prevent returning too much data 4. **Filter before grouping** - Use WHERE instead of HAVING when possible # Query Restrictions Source: https://unkey.com/docs/analytics/query-restrictions Limits, quotas, and permissions for analytics queries This page explains the restrictions, resource limits, and permissions for analytics queries. ### Only SELECT Allowed Only `SELECT` queries are permitted. All other SQL statement types return a `query_not_supported` error. **Allowed query patterns:** * `SELECT` statements * `WITH` (Common Table Expressions) * `UNION` * Subqueries * Joins * Aggregations * Window functions **Not allowed:** `INSERT`, `UPDATE`, `DELETE`, `DROP`, `ALTER`, `CREATE`, `TRUNCATE`, `GRANT`, `REVOKE` ### Table Access Control Only explicitly allowed analytics tables are accessible. Any attempt to access tables not on the allow list (including `system.*` or `information_schema.*`) will return an `invalid_table` error. ### Function Allow List Only explicitly approved functions are allowed. Any function not on this list will be rejected with an `invalid_function` error. #### Allowed Functions `count`, `sum`, `avg`, `min`, `max`, `any`, `groupArray`, `groupUniqArray`, `uniq`, `uniqExact`, `quantile`, `countIf` `now`, `now64`, `today`, `toDate`, `toDateTime`, `toDateTime64`, `toStartOfDay`, `toStartOfWeek`, `toStartOfMonth`, `toStartOfYear`, `toStartOfHour`, `toStartOfMinute`, `date_trunc`, `formatDateTime`, `fromUnixTimestamp64Milli`, `toUnixTimestamp64Milli`, `toIntervalDay`, `toIntervalWeek`, `toIntervalMonth`, `toIntervalYear`, `toIntervalHour`, `toIntervalMinute`, `toIntervalSecond`, `toIntervalMillisecond`, `toIntervalMicrosecond`, `toIntervalNanosecond`, `toIntervalQuarter` `lower`, `upper`, `substring`, `concat`, `length`, `trim`, `startsWith`, `endsWith` `round`, `floor`, `ceil`, `abs` `if`, `case`, `coalesce` `toString`, `toInt32`, `toInt64`, `toFloat64` `has`, `hasAny`, `hasAll`, `arrayJoin`, `arrayFilter` If you need a function that's not listed, please contact us at [support@unkey.com](mailto:support@unkey.com) and we'll review it for inclusion. ## Resource Limits To ensure fair usage and prevent abuse, queries are subject to resource limits: ### Execution Limits | Resource | Limit | Purpose | | ------------------------------- | --------------------- | ----------------------------- | | Max execution time | 30 seconds | Prevent long-running queries | | Max execution time (per window) | 1800 seconds (30 min) | Total execution time per hour | | Max memory usage | 1 GB | Prevent memory exhaustion | | Max result rows | 10 million | Limit result set size | ### Query Quotas | Quota | Limit | Window | | --------------------- | ----- | -------- | | Queries per workspace | 1000 | Per hour | If you need higher limits for your use case, please contact us at [support@unkey.com](mailto:support@unkey.com) with details about your specific requirements and expected query volume. ### Error Codes When limits are exceeded, you'll receive specific error codes: | Error Code | Description | Solution | | ----------------------------- | --------------------------------- | ---------------------------------------------------------- | | `query_execution_timeout` | Query took longer than 30 seconds | Add more filters, reduce time range, use aggregated tables | | `query_memory_limit_exceeded` | Query used more than 1GB memory | Reduce result set size, add LIMIT clause, use aggregation | | `query_result_rows_exceeded` | Query returned more than 10M rows | Add LIMIT clause, use aggregation, reduce time range | | `query_quota_exceeded` | Exceeded 1000 queries per hour | Wait for quota to reset, optimize query frequency | # Quick Reference Source: https://unkey.com/docs/analytics/quick-reference Fast lookup for common analytics query patterns and table selection # Analytics Quick Reference ## Essential Query Patterns ### Usage Analytics **Use for**: High-level usage metrics and health monitoring ```sql theme={null} -- Total verifications (last 7 days) SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY -- Verifications by outcome (last 30 days) SELECT outcome, SUM(count) as count FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY outcome ORDER BY count DESC -- Daily usage trend (last 30 days) SELECT time as date, SUM(count) as verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY date ORDER BY date ``` ### User Analytics **Use for**: Understanding user behavior and identifying power users ```sql theme={null} -- Top users by usage (last 30 days) SELECT external_id, SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY AND external_id != '' GROUP BY external_id ORDER BY total_verifications DESC LIMIT 10 -- Specific user activity (last 30 days) SELECT SUM(count) as total_verifications, SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) as successful FROM key_verifications_per_day_v1 WHERE external_id = 'user_123' AND time >= now() - INTERVAL 30 DAY ``` ### API Analytics **Use for**: Comparing API performance and usage ```sql theme={null} -- Usage per API (last 30 days) SELECT key_space_id, SUM(count) as total_verifications FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_space_id ORDER BY total_verifications DESC -- API success rate comparison (last 7 days) SELECT key_space_id, SUM(count) as verifications, round(SUM(CASE WHEN outcome = 'VALID' THEN count ELSE 0 END) / SUM(count) * 100, 2) as success_rate FROM key_verifications_per_day_v1 WHERE key_space_id IN ('ks_1234', 'ks_5678') AND time >= now() - INTERVAL 7 DAY GROUP BY key_space_id ``` ### Billing Queries **Use for**: Usage-based billing and credit tracking ```sql theme={null} -- Monthly credits per user SELECT external_id, toStartOfMonth(time) as month, SUM(spent_credits) as total_credits FROM key_verifications_per_day_v1 WHERE external_id != '' AND time >= toStartOfMonth(now()) GROUP BY external_id, month ORDER BY total_credits DESC -- User tier calculation (current month) SELECT external_id, SUM(spent_credits) as total_credits, CASE WHEN total_credits <= 1000 THEN 'free' WHEN total_credits <= 10000 THEN 'starter' WHEN total_credits <= 100000 THEN 'pro' ELSE 'enterprise' END as tier FROM key_verifications_per_day_v1 WHERE time >= toStartOfMonth(now()) AND external_id = 'user_123' GROUP BY external_id ``` ### Tag-Based Filtering **Use for**: Custom metadata filtering and endpoint analysis ```sql theme={null} -- Filter by single tag SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE has(tags, 'path=/api/v1/users') AND time >= now() - INTERVAL 7 DAY -- Filter by multiple tags (OR) SELECT SUM(count) as total FROM key_verifications_per_day_v1 WHERE hasAny(tags, ['path=/api/v1/users', 'path=/api/v1/posts']) AND time >= now() - INTERVAL 7 DAY -- Group by endpoint (using path tags) SELECT arrayJoin(arrayFilter(x -> startsWith(x, 'path='), tags)) as endpoint, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY endpoint ORDER BY requests DESC ``` ### Filling Gaps in Time Series **Use for**: Charts and visualizations that need consistent time intervals ```sql theme={null} -- Daily data with all days included (even zero counts) SELECT time, SUM(count) as total FROM key_verifications_per_day_v1 WHERE time >= toDate(now() - INTERVAL 30 DAY) AND time <= toDate(now()) GROUP BY time ORDER BY time ASC WITH FILL FROM toDate(now() - INTERVAL 30 DAY) TO toDate(now()) STEP INTERVAL 1 DAY ``` See [Query Examples - WITH FILL](/analytics/query-examples#filling-gaps-in-time-series-with-fill) for hourly, daily, and monthly examples with outcome breakdowns. ## Table Selection Guide Choose the right table based on your time range: | Time Range | Recommended Table | When to Use | | --------------- | --------------------------------- | ----------------------------------------- | | **\< 1 hour** | `key_verifications_v1` | Real-time analysis, detailed debugging | | **\< 24 hours** | `key_verifications_per_minute_v1` | Hourly/daily trends, recent activity | | **\< 30 days** | `key_verifications_per_hour_v1` | Daily/weekly analysis, user behavior | | **\< 1 year** | `key_verifications_per_day_v1` | Monthly/quarterly reports, billing cycles | | **> 1 year** | `key_verifications_per_month_v1` | Annual trends, long-term analytics | **Performance Tips:** * Always filter by time first (uses indexes) * Use `SUM(count)` with aggregated tables, not `COUNT(*)` * Add `LIMIT` clauses to prevent large result sets * Filter before grouping when possible ## Common Filters **Automatic filtering:** All queries are automatically filtered based on your root key permissions: * **Workspace:** All queries are scoped to your workspace (no need to filter `workspace_id`) * **API:** If your root key is scoped to a specific API (`api..read_analytics`), queries are filtered to that API's `key_space_id`. With `api.*.read_analytics` permissions, filter by `key_space_id` yourself. ### Time Ranges ```sql theme={null} -- Relative time ranges WHERE time >= now() - INTERVAL 7 DAY -- Last 7 days WHERE time >= now() - INTERVAL 24 HOUR -- Last 24 hours WHERE time >= toStartOfDay(now()) -- Today WHERE time >= toStartOfMonth(now()) -- This month ``` ### User & API Filters ```sql theme={null} -- Specific user WHERE external_id = 'user_123' -- Multiple users WHERE external_id IN ('user_123', 'user_456') -- Specific API WHERE key_space_id = 'ks_1234' -- Multiple APIs WHERE key_space_id IN ('ks_1234', 'ks_5678') ``` ### Tag Filters ```sql theme={null} -- Has specific tag WHERE has(tags, 'environment=production') -- Has any of multiple tags WHERE hasAny(tags, ['team=backend', 'team=frontend']) -- Has all specified tags WHERE hasAll(tags, ['environment=prod', 'tier=premium']) ``` ### Outcome Filters ```sql theme={null} -- Only successful verifications WHERE outcome = 'VALID' -- Only errors WHERE outcome != 'VALID' -- Specific error types WHERE outcome IN ('RATE_LIMITED', 'USAGE_EXCEEDED') ``` ## Need More Functions? → [ClickHouse Function Reference](https://clickhouse.com/docs/en/sql-reference/functions)\ → [ClickHouse SQL Documentation](https://clickhouse.com/docs/en/sql-reference) # Schema Reference Source: https://unkey.com/docs/analytics/schema-reference Tables, columns, and data types in Unkey Analytics Unkey Analytics stores verification events across multiple time-series tables for efficient querying. This reference documents all available tables and their columns. 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 | Column | Type | Description | | --------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `request_id` | String | Unique identifier for each verification request | | `time` | Int64 | Unix timestamp in milliseconds when verification occurred | | `workspace_id` | String | Workspace identifier (**automatically filtered** - you don't need to filter by this) | | `key_space_id` | String | Your KeySpace identifier (e.g., `ks_1234`). **Automatically filtered** if your root key is scoped to a single API, otherwise filter this yourself. | | `external_id` | String | Your user's identifier (e.g., `user_abc`) - use this to filter by user | | `key_id` | String | Individual API key identifier | | `outcome` | String | Verification result (see [Outcome Values](#outcome-values)) | | `region` | String | Unkey region that handled the verification | | `tags` | Array(String) | Custom tags added during verification | | `spent_credits` | Int64 | Number of credits spent on this verification (0 if no credits were spent) | ### Outcome Values The `outcome` column contains one of these values: | Outcome | Description | | -------------------------- | --------------------------------------- | | `VALID` | Key is valid and verification succeeded | | `RATE_LIMITED` | Verification exceeded rate limit | | `INVALID` | Key not found or malformed | | `EXPIRED` | Key has expired | | `DISABLED` | Key is disabled | | `INSUFFICIENT_PERMISSIONS` | Key lacks required permissions | | `FORBIDDEN` | Operation not allowed for this key | | `USAGE_EXCEEDED` | Key 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 | Column | Type | Description | | --------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------- | | `time` | DateTime/Date | **DateTime** for minute/hour tables, **Date** for day/month tables | | `workspace_id` | String | Workspace identifier (**automatically filtered** - you don't need to filter by this) | | `key_space_id` | String | API identifier. **Automatically filtered** if your root key is scoped to a single API, otherwise filter this yourself. | | `external_id` | String | Your user identifier | | `key_id` | String | API key identifier | | `outcome` | String | Verification outcome (VALID, RATE\_LIMITED, INVALID, etc.) | | `tags` | Array | Tags associated with verifications | | `count` | UInt64 | Total verification count for this aggregation | | `spent_credits` | UInt64 | Total 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 API and User You can use your familiar identifiers directly in queries: * **`key_space_id`** - Your API identifier (e.g., `ks_1234`). Find this in your API settings. * **`external_id`** - Your user identifiers (e.g., `user_abc123`) from your application All standard comparison operators are supported: `=`, `!=`, `<`, `>`, `<=`, `>=`, `IN`, `NOT IN` ### Filter by API ```sql theme={null} SELECT COUNT(*) FROM key_verifications_v1 WHERE key_space_id = 'ks_1234' ``` ### Filter by User ```sql theme={null} SELECT COUNT(*) FROM key_verifications_v1 WHERE external_id = 'user_abc123' ``` ### Multiple Values ```sql theme={null} SELECT COUNT(*) FROM key_verifications_v1 WHERE key_space_id IN ('ks_1234', 'ks_5678') AND external_id IN ('user_abc', 'user_xyz') ``` ## Working with Tags Tags are stored as `Array(String)` and require array functions to query. ### Check if tag exists ```sql theme={null} SELECT COUNT(*) FROM key_verifications_v1 WHERE has(tags, 'path=/api/users') ``` ### Check if any tag exists ```sql theme={null} SELECT COUNT(*) FROM key_verifications_v1 WHERE hasAny(tags, ['environment=prod', 'environment=staging']) ``` ### Check if all tags exist ```sql theme={null} SELECT COUNT(*) FROM key_verifications_v1 WHERE hasAll(tags, ['environment=production', 'team=backend']) ``` ### Extract and group by tags ```sql theme={null} SELECT arrayJoin(tags) as tag, COUNT(*) as count FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY tag ORDER BY count DESC ``` ### Filter tags with pattern ```sql theme={null} -- Get all tags starting with "path=" SELECT arrayJoin(arrayFilter(x -> startsWith(x, 'path='), tags)) as path, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY path ``` ## 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 ```sql theme={null} SELECT now() as current_datetime SELECT toUnixTimestamp(now()) * 1000 as current_millis ``` ### Time Ranges (Raw Table) For the raw `key_verifications_v1` table, compare `time` with millisecond timestamps: ```sql theme={null} -- Last hour WHERE time >= toUnixTimestamp(now() - INTERVAL 1 HOUR) * 1000 -- Last 24 hours WHERE time >= toUnixTimestamp(now() - INTERVAL 24 HOUR) * 1000 -- Last 7 days WHERE time >= toUnixTimestamp(now() - INTERVAL 7 DAY) * 1000 -- Last 30 days WHERE time >= toUnixTimestamp(now() - INTERVAL 30 DAY) * 1000 -- This month WHERE time >= toUnixTimestamp(toStartOfMonth(now())) * 1000 -- Today WHERE time >= toUnixTimestamp(toStartOfDay(now())) * 1000 ``` ### Time Ranges (Aggregated Tables) For aggregated tables, use DateTime comparisons directly: ```sql theme={null} -- Last 7 days WHERE time >= now() - INTERVAL 7 DAY -- This month WHERE time >= toStartOfMonth(now()) -- Today WHERE time >= toStartOfDay(now()) ``` ### Time Rounding (Raw Table) ```sql theme={null} -- Round to start of hour SELECT toStartOfHour(toDateTime(time / 1000)) as hour -- Round to start of day SELECT toStartOfDay(toDateTime(time / 1000)) as day -- Round to start of month SELECT toStartOfMonth(toDateTime(time / 1000)) as month -- Convert to date SELECT toDate(toDateTime(time / 1000)) as date ``` ### Specific Date Ranges ```sql theme={null} -- Between specific dates (Unix milliseconds) WHERE time >= 1704067200000 -- Jan 1, 2024 00:00:00 UTC AND time < 1735689600000 -- Jan 1, 2025 00:00:00 UTC ``` ## Common ClickHouse Functions ### Aggregate Functions | Function | Description | Example | | ----------- | ----------------- | ----------------------------------------------------------- | | `COUNT()` | Count rows | `SELECT COUNT(*) FROM key_verifications_v1` | | `SUM()` | Sum values | `SELECT SUM(valid_count) FROM key_verifications_per_day_v1` | | `AVG()` | Average | `SELECT AVG(spent_credits) FROM key_verifications_v1` | | `MIN()` | Minimum value | `SELECT MIN(time) FROM key_verifications_v1` | | `MAX()` | Maximum value | `SELECT MAX(time) FROM key_verifications_v1` | | `countIf()` | Conditional count | `SELECT countIf(outcome = 'VALID')` | | `uniq()` | Count distinct | `SELECT uniq(key_id) FROM key_verifications_v1` | ### String Functions | Function | Description | Example | | -------------- | -------------------- | ------------------------------------- | | `lower()` | Convert to lowercase | `WHERE lower(outcome) = 'valid'` | | `upper()` | Convert to uppercase | `WHERE upper(region) = 'US-EAST-1'` | | `concat()` | Concatenate strings | `SELECT concat(region, '-', outcome)` | | `substring()` | Extract substring | `SELECT substring(key_id, 1, 8)` | | `startsWith()` | Check prefix | `WHERE startsWith(key_id, 'key_')` | ### Array Functions | Function | Description | Example | | --------------- | ------------------ | ---------------------------------------------------- | | `has()` | Check element | `WHERE has(tags, 'environment=production')` | | `hasAny()` | Check any element | `WHERE hasAny(tags, ['team=backend', 'team=api'])` | | `hasAll()` | Check all elements | `WHERE hasAll(tags, ['environment=prod', 'tier=1'])` | | `arrayJoin()` | Expand array | `SELECT arrayJoin(tags) as tag` | | `arrayFilter()` | Filter array | `arrayFilter(x -> startsWith(x, 'path='), tags)` | | `length()` | Array length | `WHERE length(tags) > 0` | ### Math Functions | Function | Description | Example | | --------- | -------------- | ---------------------------------------------------------- | | `round()` | Round number | `SELECT round(AVG(spent_credits), 2)` | | `floor()` | Round down | `SELECT floor(spent_credits / 100) * 100 as credit_bucket` | | `ceil()` | Round up | `SELECT ceil(spent_credits)` | | `abs()` | Absolute value | `SELECT abs(difference)` | ### Conditional Functions | Function | Description | Example | | -------- | --------------- | --------------------------------------------------------------- | | `if()` | If-then-else | `SELECT if(outcome = 'VALID', 1, 0)` | | `CASE` | Multi-condition | `CASE 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 | Resource | Limit | Error Code | | ---------------- | ---------- | ----------------------------- | | Execution time | 30 seconds | `query_execution_timeout` | | Memory usage | 1 GB | `query_memory_limit_exceeded` | | Rows to read | 10 million | `query_rows_limit_exceeded` | | Queries per hour | 1000 | `query_quota_exceeded` | See [Query Restrictions](/analytics/query-restrictions) for more details on query limits and restrictions. # Troubleshooting Source: https://unkey.com/docs/analytics/troubleshooting Common issues and solutions for analytics queries # Analytics Troubleshooting ## Getting Empty Results? **Check these common causes:** ### Time Range Issues * Your workspace might be new with no verification data yet * Try a shorter time range: `WHERE time >= now() - INTERVAL 1 HOUR` * Verify your time filters are working: `SELECT MAX(time) as latest FROM key_verifications_v1` ### Filter Problems * Wrong `key_space_id` - Find your API ID in dashboard settings * Empty `external_id` values - Filter them out: `WHERE external_id != ''` * Case sensitivity - Check exact values: `SELECT DISTINCT external_id FROM key_verifications_v1 LIMIT 10` ### Data Availability * Analytics may have a few minutes delay * Check recent data: `SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 HOUR` ## Queries Timing Out? **Try these optimizations:** ### Use Aggregated Tables ```sql theme={null} -- Instead of raw table for long ranges: -- Slow for 7+ days SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY -- Fast for 7+ days SELECT SUM(count) FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ### Add Time Filters First ```sql theme={null} -- Always filter by time early (uses indexes) WHERE time >= now() - INTERVAL 7 DAY AND external_id = 'user_123' -- Additional filters after time ``` ### Limit Result Size ```sql theme={null} -- Prevent large result sets SELECT external_id, SUM(count) as usage FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY external_id ORDER BY usage DESC LIMIT 100 -- Add LIMIT for large datasets ``` ## API Errors? **Common fixes:** ### Missing Property 'query' ```json theme={null} // Wrong field name {"sql": "SELECT COUNT(*) FROM key_verifications_v1"} // Correct field name {"query": "SELECT COUNT(*) FROM key_verifications_v1"} ``` See [invalid\_input](/errors/unkey/application/invalid_input) for API request format issues. ### Permission Denied * Ensure your root key has `api.*.read_analytics` or `api..read_analytics` * Check key is not expired or revoked * Verify workspace ID matches your key's permissions See [forbidden](/errors/unkey/authorization/forbidden) for permission issues. ### Invalid Function Error * Check [Query Restrictions](/analytics/query-restrictions#function-allow-list) for allowed functions * Some ClickHouse functions are blocked for security * Use alternative approaches from [Quick Reference](/analytics/quick-reference) * See [invalid\_analytics\_function](/errors/user/bad_request/invalid_analytics_function) for details ### Invalid Table Error * Only analytics tables are accessible (no `system.*` or `information_schema.*`) * Use table names from [Schema Reference](/analytics/schema-reference) * See [invalid\_analytics\_table](/errors/user/bad_request/invalid_analytics_table) for details ### Query Not Supported * Only SELECT queries are allowed in analytics * INSERT, UPDATE, DELETE, etc. are blocked * See [invalid\_analytics\_query\_type](/errors/user/bad_request/invalid_analytics_query_type) for details ## Performance Problems? **Optimization tips:** ### Choose Right Table * `< 1 hour`: `key_verifications_v1` (raw) * `< 24 hours`: `key_verifications_per_minute_v1` * `< 30 days`: `key_verifications_per_hour_v1` * `< 1 year`: `key_verifications_per_day_v1` * `> 1 year`: `key_verifications_per_month_v1` ### Filter Before Grouping ```sql theme={null} -- Less efficient SELECT external_id, SUM(count) as usage FROM key_verifications_per_day_v1 GROUP BY external_id HAVING SUM(count) > 1000 -- More efficient SELECT external_id, SUM(count) as usage FROM key_verifications_per_day_v1 WHERE time >= now() - INTERVAL 30 DAY -- Filter first GROUP BY external_id HAVING SUM(count) > 1000 ``` ### Avoid SELECT \* * Only select columns you need * Reduces memory usage and network transfer For complete error reference, see the [Error Documentation](/errors/overview). If you continue having issues, contact us at [support@unkey.com](mailto:support@unkey.com) with your query and error details. # /v1/analytics.* Source: https://unkey.com/docs/api-reference/v1/migration/analytics Migrate analytics queries from v1 to v2 for enhanced SQL-based querying 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: ```bash v1 Analytics Access theme={null} curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications?apiId=api_123&start=1620000000000" \ -H "Authorization: Bearer " ``` ### 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.com](mailto:support@unkey.com) with: * Your workspace ID * Your use case (billing, dashboards, reporting, etc.) * Expected query volume **Comprehensive Analytics Documentation Available** This 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](/analytics) section, starting with [Getting Started](/analytics/getting-started). ```bash v2 Example theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -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 Permission | V2 Permission | Description | | ---------------------- | ---------------------------- | --------------------------------- | | `api.*.read_api` | `api.*.read_analytics` | Access analytics for all APIs | | `api.api_123.read_api` | `api.api_123.read_analytics` | Access 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: ```bash v1 Request Structure theme={null} curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications" \ -H "Authorization: Bearer " \ -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: ```bash v2 Request Structure theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -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: ```json v1 Response Structure theme={null} [ { "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: ```json v2 Response Structure theme={null} { "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 ```bash theme={null} curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications?apiId=api_123&start=1620000000000&groupBy=day" ``` #### V2: SQL Equivalent ```bash theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -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 ```bash theme={null} curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications?apiId=api_123&externalId=user_456&groupBy=day" ``` #### V2: SQL Filter ```bash theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -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 ```bash theme={null} curl -X GET "https://api.unkey.dev/v1/analytics.getVerifications?apiId=api_123&groupBy=day" ``` #### V2: Custom Outcome Aggregation ```bash theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -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 ```bash theme={null} 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 ```bash theme={null} curl -X POST https://api.unkey.com/v2/analytics.getVerifications \ -H "Authorization: Bearer " \ -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 * **Analytics Documentation:** [https://unkey.com/docs/analytics](https://unkey.com/docs/analytics) * [Getting Started](/analytics/getting-started) - Request access and run your first query * [Schema Reference](/analytics/schema-reference) - Complete table and column documentation * [Query Examples](/analytics/query-examples) - 30+ ready-to-use SQL queries * [Quick Reference](/analytics/quick-reference) - Common patterns and functions * [Query Restrictions](/analytics/query-restrictions) - Limits and error codes * [Troubleshooting](/analytics/troubleshooting) - Common issues and solutions * **Migration Support:** [https://unkey.com/docs/api-reference/v1/migration](https://unkey.com/docs/api-reference/v1/migration) * **Discord:** [https://unkey.com/discord](https://unkey.com/discord) * **Email Support:** [support@unkey.com](mailto:support@unkey.com) * **Book a Call:** [https://cal.com/team/unkey/founders](https://cal.com/team/unkey/founders) - Schedule time with our team for migration help # /v1/apis.* Source: https://unkey.com/docs/api-reference/v1/migration/apis Migrate API namespace management endpoints from v1 to v2 This guide covers API namespace management endpoints for creating and managing API containers that organize your keys. ## Overview API endpoints manage the namespaces that contain your keys, providing CRUD operations for API management and key listing. ### Key Changes in v2: * **Response format**: Direct responses → `{meta, data}` envelope * **HTTP methods**: Some GET → POST changes for consistency * **Enhanced responses**: Request IDs for debugging and pagination metadata * **Consistent structure**: All responses follow same envelope pattern ### Migration Impact: * **Existing in v1**: Full API CRUD operations and key listing functionality * **Enhanced in v2**: Improved response format, better pagination, and enhanced filtering * **Maintained in v2**: All core API management functionality with consistent request patterns *** ## POST /v1/apis.createApi → POST /v2/apis.createApi **Key Changes:** * Response format: Direct response → `{meta, data}` envelope ```json title="Create API Request" icon="plus-circle" theme={null} { "name": "Production API" } ``` ```json title="Create API Response Diff" icon="database" expandable theme={null} // v1 Response (direct, no wrapper) { "apiId": "api_1234567890abcdef" // [!code --] } // v2 Response (with meta envelope) { "meta": { // [!code ++] "requestId": "req_createapi123" // [!code ++] }, // [!code ++] "data": { // [!code ++] "apiId": "api_1234567890abcdef" // [!code ++] } // [!code ++] } ``` ```bash title="API Endpoint & Domain Change" icon="arrow-right" theme={null} # v1: api.unkey.dev domain curl -X POST https://api.unkey.dev/v1/apis.createApi # [!code --] -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"name": "Production API"}' # v2: api.unkey.com domain curl -X POST https://api.unkey.com/v2/apis.createApi # [!code ++] -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"name": "Production API"}' ``` *** ## GET /v1/apis.getApi → POST /v2/apis.getApi **Key Changes:** * HTTP method: GET → POST * Request body format required instead of query parameters * Response format: Direct response → `{meta, data}` envelope ```bash title="HTTP Method & Parameter Change" icon="arrow-right" theme={null} # v1: GET with query parameters curl -X GET "https://api.unkey.dev/v1/apis.getApi?apiId=api_123" # [!code --] -H "Authorization: Bearer " # v2: POST with request body curl -X POST https://api.unkey.com/v2/apis.getApi # [!code ++] -H "Authorization: Bearer " -H "Content-Type: application/json" # [!code ++] -d '{"apiId": "api_123"}' # [!code ++] ``` ```json title="Get API Response Diff" icon="database" expandable theme={null} // v1 Response (direct, no wrapper) { "id": "api_123", // [!code --] "workspaceId": "ws_xyz789", // [!code --] "name": "Production API" // [!code --] } // v2 Response (with meta envelope, no workspaceId) { "meta": { // [!code ++] "requestId": "req_getapi456" // [!code ++] }, // [!code ++] "data": { // [!code ++] "id": "api_123", // [!code ++] "name": "Production API" // [!code ++] } // [!code ++] } ``` ```bash title="Complete Examples" icon="terminal" theme={null} # v1: GET with query parameters curl -X GET "https://api.unkey.dev/v1/apis.getApi?apiId=api_123" # [!code --] -H "Authorization: Bearer " # [!code --] # v2: POST with request body curl -X POST https://api.unkey.com/v2/apis.getApi # [!code ++] -H "Authorization: Bearer " -H "Content-Type: application/json" # [!code ++] -d '{"apiId": "api_123"}' # [!code ++] ``` *** ## GET /v1/apis.listKeys → POST /v2/apis.listKeys **Key Changes:** * HTTP method: GET → POST * Request body format required instead of query parameters * Enhanced filtering and pagination options * Response format: Direct response → `{meta, data}` envelope ```json title="List Keys Request Diff" icon="list" expandable theme={null} // v1: Query parameters only // ?apiId=api_123&limit=100 // v2: Request body with enhanced options { "apiId": "api_123", "limit": 100, "cursor": "optional_cursor_for_pagination", // [!code ++] "externalId": "optional_filter_by_external_id" // [!code ++] } ``` ```json title="List Keys Response Diff" icon="database" expandable theme={null} // v1 Response (direct structure with metadata) { "keys": [ // [!code --] { // [!code --] "id": "key_123", // [!code --] "name": "Production Key", // [!code --] "start": "prod_1234" // [!code --] } // [!code --] ], // [!code --] "cursor": "next_page_cursor", // [!code --] "total": 42 // [!code --] } // v2 Response (meta envelope with direct key array) { "meta": { // [!code ++] "requestId": "req_listkeys789" // [!code ++] }, // [!code ++] "data": [ // [!code ++] { // [!code ++] "keyId": "key_123", // [!code ++] "name": "Production Key", // [!code ++] "start": "prod_1234", // [!code ++] "externalId": "customer_789", // [!code ++] "enabled": true // [!code ++] } // [!code ++] ], // [!code ++] "pagination": { // [!code ++] "cursor": "next_page_cursor_here", // [!code ++] "hasMore": true // [!code ++] } // [!code ++] } ``` ```json title="Enhanced Filtering Options" icon="filter" theme={null} // Basic listing { "apiId": "api_123", "limit": 50 } // Filter by external ID { "apiId": "api_123", "externalId": "customer_789", // [!code focus] "limit": 50 } // Pagination { "apiId": "api_123", "cursor": "cursor_from_previous_response", // [!code focus] "limit": 50 } ``` ```bash title="Method & Parameter Changes" icon="arrow-right" theme={null} # v1: GET with query parameters curl -X GET "https://api.unkey.dev/v1/apis.listKeys?apiId=api_123&limit=100" # [!code --] -H "Authorization: Bearer " # [!code --] # v2: POST with enhanced request body curl -X POST https://api.unkey.com/v2/apis.listKeys # [!code ++] -H "Authorization: Bearer " -H "Content-Type: application/json" # [!code ++] -d '{"apiId": "api_123", "limit": 100, "cursor": "optional_cursor", "externalId": "optional_filter"}' # [!code ++] ``` *** ## POST /v1/apis.deleteApi → POST /v2/apis.deleteApi **Key Changes:** * Response format: Direct response → `{meta, data}` envelope ```json title="Delete API Request" icon="trash" theme={null} { "apiId": "api_123" } ``` ```json title="Delete API Response Diff" icon="check-circle" theme={null} // v1 Response (empty object) {} // [!code --] // v2 Response (meta envelope with empty data) { "meta": { // [!code ++] "requestId": "req_deleteapi999" // [!code ++] }, // [!code ++] "data": {} // [!code ++] } ``` ```bash title="Domain Change Only" icon="arrow-right" theme={null} # v1: api.unkey.dev domain curl -X POST https://api.unkey.dev/v1/apis.deleteApi # [!code --] -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"apiId": "api_123"}' # v2: api.unkey.com domain curl -X POST https://api.unkey.com/v2/apis.deleteApi # [!code ++] -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"apiId": "api_123"}' ``` *** ## POST /v1/apis.deleteKeys → Removed in v2 **Purpose:** Delete all keys within an API namespace. **Migration Path:** Use individual `POST /v2/keys.deleteKey` calls for each key or delete the entire API with `POST /v2/apis.deleteApi`. ```bash title="v1: Delete all keys in API" icon="trash" theme={null} curl -X POST https://api.unkey.dev/v1/apis.deleteKeys -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"apiId": "api_123"}' ``` ```bash title="Option 1: Delete Individual Keys" icon="key" theme={null} # First, list keys to get their IDs curl -X POST https://api.unkey.com/v2/apis.listKeys -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"apiId": "api_123"}' # Then delete each key individually curl -X POST https://api.unkey.com/v2/keys.deleteKey -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"keyId": "key_123"}' ``` ```bash title="Option 2: Delete Entire API" icon="database" theme={null} curl -X POST https://api.unkey.com/v2/apis.deleteApi -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"apiId": "api_123"}' ``` ```typescript title="Programmatic Migration Example" theme={null} // v2: Migration helper function async function deleteAllKeysInApi(apiId: string) { // List all keys first const response = await fetch('/v2/apis.listKeys', { method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ apiId }) }); const { data } = await response.json(); // Delete each key individually for (const key of data) { await fetch('/v2/keys.deleteKey', { method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ keyId: key.keyId }) }); } } ``` # Errors Source: https://unkey.com/docs/api-reference/v1/migration/errors This guide covers changes to error response formats, error codes, and debugging capabilities when migrating from v1 to v2. ## Overview Error handling changes affect all API endpoints, providing improved debugging capabilities and standardized error responses. ### Key Changes in v2: * **Standardized format**: All errors use `{meta, error}` envelope with consistent structure * **Request IDs**: Every response includes `meta.requestId` for debugging * **Enhanced error structure**: Errors follow RFC 7807 Problem Details format * **Better debugging**: Improved error context and troubleshooting information For detailed information about specific error codes and troubleshooting, see the [Error Documentation](/errors/overview). *** ## Error Response Format Changes ### v1 Error Format → v2 Error Format ```json title="Error Response Migration" icon="triangle-exclamation" theme={null} // v1 Error Response { "error": { // [!code --] "code": "NOT_FOUND", // [!code --] "message": "Key not found" // [!code --] } // [!code --] } // v2 Error Response { "meta": { // [!code ++] "requestId": "req_error123abc" // [!code ++] }, // [!code ++] "error": { // [!code ++] "title": "Not Found", // [!code ++] "detail": "The requested key was not found", // [!code ++] "status": 404, // [!code ++] "type": "https://unkey.com/docs/errors/unkey/data/key_not_found" // [!code ++] } // [!code ++] } ``` ```json title="Validation Error Migration" icon="exclamation-circle" expandable theme={null} // v1 Validation Error { "error": { // [!code --] "code": "BAD_REQUEST", // [!code --] "message": "Invalid input" // [!code --] } // [!code --] } // v2 Validation Error { "meta": { // [!code ++] "requestId": "req_validation456" // [!code ++] }, // [!code ++] "error": { // [!code ++] "title": "Bad Request", // [!code ++] "detail": "The request contains invalid parameters", // [!code ++] "status": 400, // [!code ++] "type": "https://unkey.com/docs/errors/unkey/application/invalid_input", // [!code ++] "errors": [ // [!code ++] { // [!code ++] "location": "body.apiId", // [!code ++] "message": "Must be at least 3 characters long", // [!code ++] "fix": "Ensure the API ID is a valid identifier" // [!code ++] } // [!code ++] ] // [!code ++] } // [!code ++] } ``` *** ## Error Code Mapping Table The following table provides a comprehensive mapping of v1 error codes to their v2 equivalents: ### HTTP Status Errors | v1 Error Code | HTTP Status | v2 Error Type | v2 Category | Description | | ----------------------- | ----------- | --------------------------------------------------------------------- | -------------- | -------------------------------------------------- | | `BAD_REQUEST` | 400 | `https://unkey.com/docs/errors/unkey/application/invalid_input` | Application | Invalid request parameters or malformed input | | `UNAUTHORIZED` | 401 | `https://unkey.com/docs/errors/unkey/authentication/key_not_found` | Authentication | Missing or invalid authentication | | `FORBIDDEN` | 403 | `https://unkey.com/docs/errors/unkey/authorization/forbidden` | Authorization | Insufficient permissions for the requested action | | `NOT_FOUND` | 404 | `https://unkey.com/docs/errors/unkey/data/key_not_found` | Data | Requested resource does not exist | | `CONFLICT` | 409 | `https://unkey.com/docs/errors/unkey/data/conflict` | Data | Resource conflict (e.g., duplicate creation) | | `PRECONDITION_FAILED` | 412 | `https://unkey.com/docs/errors/unkey/application/precondition_failed` | Application | Required preconditions not met | | `TOO_MANY_REQUESTS` | 429 | `https://unkey.com/docs/errors/unkey/application/rate_limited` | Application | Rate limit exceeded | | `INTERNAL_SERVER_ERROR` | 500 | `https://unkey.com/docs/errors/unkey/application/internal_error` | Application | Unexpected server error | | `DELETE_PROTECTED` | 403 | `https://unkey.com/docs/errors/unkey/authorization/delete_protected` | Authorization | Resource cannot be deleted due to protection rules | ### Key Verification Specific Codes | v1 Verification Code | v2 Error Type | Description | Migration Notes | | -------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------ | | `VALID` | N/A | Key is valid and verification successful | No error - successful response | | `NOT_FOUND` | `https://unkey.com/docs/errors/unkey/data/key_not_found` | Key does not exist or has been deleted | Same as HTTP 404 NOT\_FOUND | | `FORBIDDEN` | `https://unkey.com/docs/errors/unkey/authorization/forbidden` | Key is not allowed to access this API | Same as HTTP 403 FORBIDDEN | | `USAGE_EXCEEDED` | `https://unkey.com/docs/errors/unkey/data/usage_exceeded` | Key has exceeded its usage limit | New specific error type in v2 | | `RATE_LIMITED` | `https://unkey.com/docs/errors/unkey/application/rate_limited` | Key has been rate limited | Same as HTTP 429 TOO\_MANY\_REQUESTS | | `UNAUTHORIZED` | `https://unkey.com/docs/errors/unkey/authentication/unauthorized` | Key authentication failed | Same as HTTP 401 UNAUTHORIZED | | `DISABLED` | `https://unkey.com/docs/errors/unkey/authorization/key_disabled` | Key has been disabled | New specific error type in v2 | | `INSUFFICIENT_PERMISSIONS` | `https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions` | Key lacks required permissions | Enhanced RBAC error in v2 | | `EXPIRED` | `https://unkey.com/docs/errors/unkey/data/key_expired` | Key has expired | New specific error type in v2 | ### Migration Code Examples ```typescript title="v1 vs v2 Error Handling" theme={null} // v1: Simple error code checking const response = await fetch('/v1/keys.verifyKey', { /* ... */ }); const data = await response.json(); if (data.error) { switch (data.error.code) { case 'NOT_FOUND': console.log('Key not found'); break; case 'RATE_LIMITED': console.log('Rate limited'); break; default: console.log('Unknown error:', data.error.message); } } // v2: RFC 7807 error handling const response = await fetch('/v2/keys.verifyKey', { /* ... */ }); const result = await response.json(); if (result.error) { const { title, detail, status, type } = result.error; const requestId = result.meta.requestId; // Log for debugging console.log(`Error ${status}: ${title} - ${detail} (Request: ${requestId})`); // Handle by category if (type.includes('/authentication/')) { console.log('Authentication error'); } else if (type.includes('/authorization/')) { console.log('Authorization error'); } else if (type.includes('/data/')) { console.log('Data error'); } } ``` ```typescript title="Error Category Helper" theme={null} function getErrorCategory(v2ErrorType: string): string { if (v2ErrorType.includes('/authentication/')) return 'authentication'; if (v2ErrorType.includes('/authorization/')) return 'authorization'; if (v2ErrorType.includes('/application/')) return 'application'; if (v2ErrorType.includes('/data/')) return 'data'; return 'unknown'; } function isRetryableError(v2ErrorType: string): boolean { // Rate limits and internal errors are retryable return v2ErrorType.includes('rate_limited') || v2ErrorType.includes('internal_error'); } ``` *** ## Error Documentation For comprehensive information about specific error codes, causes, and resolution steps, refer to the error documentation: ### Common Error Categories * **[Application Errors](/errors/unkey/application/invalid_input)**: Invalid input, assertion failures, service unavailable * **[Authentication Errors](/errors/unkey/authentication/key_not_found)**: Missing, malformed, or invalid keys * **[Authorization Errors](/errors/unkey/authorization/forbidden)**: Insufficient permissions, disabled keys, workspace access * **[Data Errors](/errors/unkey/data/key_not_found)**: Resource not found, conflicts, data validation issues ### Error Troubleshooting * **Request IDs**: Always include the `meta.requestId` when contacting support * **Error Types**: Use the `type` URL for detailed documentation about specific errors * **Validation Errors**: Check the `errors` array for field-specific validation failures * **Status Codes**: HTTP status codes indicate the general category of the error #### Common Error Migration Issues **Problem:** Error handling code not working after migration **Symptoms:** * Errors not being caught properly * Missing error details that were available in v1 * Unable to determine error type or category **Solutions:** 1. **Update Error Access Pattern** ```typescript theme={null} // ❌ v1 pattern if (response.error) { console.log('Error:', response.error.code); } // ✅ v2 pattern if (response.error) { console.log('Error:', response.error.type); console.log('Request ID:', response.meta.requestId); } ``` 2. **Handle New Error Structure** ```typescript theme={null} // v2 error handling with all fields if (response.error) { const { title, detail, status, type } = response.error; // Log complete error information console.error(`${title} (${status}): ${detail}`); console.error(`Error Type: ${type}`); console.error(`Request ID: ${response.meta.requestId}`); // Handle validation errors if (response.error.errors) { response.error.errors.forEach(err => { console.error(`Field ${err.location}: ${err.message}`); }); } } ``` 3. **Error Categorization** ```typescript theme={null} function categorizeError(errorType: string): string { if (errorType.includes('/authentication/')) return 'auth'; if (errorType.includes('/authorization/')) return 'permission'; if (errorType.includes('/application/')) return 'client'; if (errorType.includes('/data/')) return 'resource'; return 'unknown'; } ``` 4. **Retry Logic for Retryable Errors** ```typescript theme={null} function isRetryable(errorType: string): boolean { return errorType.includes('rate_limited') || errorType.includes('internal_error'); } if (response.error && isRetryable(response.error.type)) { // Implement retry logic setTimeout(() => retryRequest(), 1000); } ``` ### Migration Considerations When migrating error handling code: * Update error parsing to access `response.error` instead of direct error access * Extract `meta.requestId` for logging and support requests * Handle the new RFC 7807 format with `title`, `detail`, `status`, and `type` fields * Process validation errors from the `errors` array for detailed field-level feedback # /v1/identities.* Source: https://unkey.com/docs/api-reference/v1/migration/identities Changes and improvements to identity endpoints from v1 to v2 This guide covers the changes to identity endpoints between v1 and v2. Both versions provide full identity management capabilities with different API patterns. ## Overview ### What Changed in v2: * **HTTP methods**: GET endpoints changed to POST for consistency * **Request format**: Query parameters moved to request body * **Response format**: Direct response wrapped in structured `{meta, data}` format * **Parameter flexibility**: Enhanced parameter handling (accepts both identityId and externalId) * **Error handling**: Improved error response structure ### Migration Impact: * **v1**: Mixed GET/POST with query parameters and direct responses * **v2**: All POST with request bodies and structured `{meta, data}` responses * **Benefit**: Consistent API patterns, better error tracking, enhanced flexibility *** ## POST /v1/identities.createIdentity → POST /v2/identities.createIdentity **Purpose:** Create a new identity with metadata and rate limits. **Changes:** Request format unchanged, response wrapped in structured format. ```json title="v1 vs v2: Create Identity Request" expandable theme={null} // v1: POST /v1/identities.createIdentity { "externalId": "user_123", "meta": { "email": "user@example.com", "plan": "pro" }, "ratelimits": [ { "name": "requests", "limit": 1000, "duration": 3600000 } ] } // v2: POST /v2/identities.createIdentity (same request format) { "externalId": "user_123", "meta": { "email": "user@example.com", "plan": "pro" }, "ratelimits": [ { "name": "requests", "limit": 1000, "duration": 3600000 } ] } ``` ```json title="v1 vs v2: Create Identity Response" expandable theme={null} // v1: Direct response { "identityId": "id_abc123def456" } // v2: Structured response with meta wrapper { "meta": { "requestId": "req_createidentity123" }, "data": { "identityId": "id_abc123def456" } } ``` ```bash title="Migration Examples" theme={null} # v1: Create identity curl -X POST https://api.unkey.dev/v1/identities.createIdentity \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"externalId": "user_123", "meta": {"email": "user@example.com"}, "ratelimits": [{"name": "requests", "limit": 1000, "duration": 3600000}]}' # v2: Create identity (same request, structured response) curl -X POST https://api.unkey.com/v2/identities.createIdentity \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"externalId": "user_123", "meta": {"email": "user@example.com"}, "ratelimits": [{"name": "requests", "limit": 1000, "duration": 3600000}]}' ``` *** ## GET /v1/identities.getIdentity → POST /v2/identities.getIdentity **Purpose:** Retrieve identity data by ID or external ID. **Key Changes:** GET with query parameters → POST with request body, response format enhanced. ```bash title="v1 vs v2: Get Identity Request" theme={null} # v1: GET with query parameters curl -X GET "https://api.unkey.dev/v1/identities.getIdentity?externalId=user_123" \ -H "Authorization: Bearer " # Alternative v1: Using identityId curl -X GET "https://api.unkey.dev/v1/identities.getIdentity?identityId=identity_123" \ -H "Authorization: Bearer " # v2: POST with request body (accepts both ID types) curl -X POST https://api.unkey.com/v2/identities.getIdentity \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"identity": "user_123"}' ``` ```json title="v1 vs v2: Get Identity Response" expandable theme={null} // v1: Direct response { "id": "id_abc123def456", "externalId": "user_123", "meta": { "email": "user@example.com", "plan": "pro" }, "ratelimits": [ { "name": "requests", "limit": 1000, "duration": 3600000 } ] } // v2: Structured response with meta wrapper { "meta": { "requestId": "req_getidentity456" }, "data": { "id": "id_abc123def456", "externalId": "user_123", "meta": { "email": "user@example.com", "plan": "pro" }, "ratelimits": [ { "id": "rl_abcdef123456", "name": "requests", "limit": 1000, "duration": 3600000 } ] } } ``` *** ## GET /v1/identities.listIdentities → POST /v2/identities.listIdentities **Purpose:** Get paginated list of all identities. **Key Changes:** GET with query parameters → POST with request body, enhanced pagination structure. ```bash title="v1 vs v2: List Identities Request" theme={null} # v1: GET with query parameters curl -X GET "https://api.unkey.dev/v1/identities.listIdentities?limit=50&cursor=eyJrZXkiOiJrZXlfMTIzNCJ9" \ -H "Authorization: Bearer " # v2: POST with request body curl -X POST https://api.unkey.com/v2/identities.listIdentities \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"limit": 50, "cursor": "eyJrZXkiOiJrZXlfMTIzNCJ9"}' ``` ```json title="v1 vs v2: List Identities Response" expandable theme={null} // v1: Direct response with total count { "identities": [ { "id": "id_abc123", "externalId": "user_123", "ratelimits": [ { "name": "requests", "limit": 1000, "duration": 3600000 } ] } ], "cursor": "eyJrZXkiOiJrZXlfMTIzNCJ9", "total": 42 } // v2: Structured response with pagination object { "meta": { "requestId": "req_listidentities789" }, "data": [ { "id": "id_abc123", "externalId": "user_123", "meta": { "email": "user@example.com", "plan": "pro" }, "ratelimits": [ { "id": "rl_abcdef123456", "name": "requests", "limit": 1000, "duration": 3600000 } ] } ], "pagination": { "cursor": "eyJrZXkiOiJrZXlfMTIzNCJ9" } } ``` *** ## POST /v1/identities.updateIdentity → POST /v2/identities.updateIdentity **Purpose:** Update identity metadata and rate limits. **Changes:** Enhanced parameter flexibility, structured response format. ```json title="v1 vs v2: Update Identity Request" expandable theme={null} // v1: Requires specific ID field { "identityId": "id_abc123", // or "externalId": "user_123" "meta": { "email": "user@example.com", "plan": "enterprise" }, "ratelimits": [ { "name": "requests", "limit": 5000, "duration": 3600000 } ] } // v2: Flexible identity parameter { "identity": "user_123", // accepts both identityId or externalId "meta": { "email": "user@example.com", "plan": "enterprise" }, "ratelimits": [ { "name": "requests", "limit": 5000, "duration": 3600000 } ] } ``` ```json title="v1 vs v2: Update Identity Response" expandable theme={null} // v1: Direct response { "id": "id_abc123def456", "externalId": "user_123", "meta": { "email": "user@example.com", "plan": "enterprise" }, "ratelimits": [ { "name": "requests", "limit": 5000, "duration": 3600000 } ] } // v2: Structured response with meta wrapper { "meta": { "requestId": "req_updateidentity789" }, "data": { "id": "id_abc123def456", "externalId": "user_123", "meta": { "email": "user@example.com", "plan": "enterprise" }, "ratelimits": [ { "id": "rl_abcdef123456", "name": "requests", "limit": 5000, "duration": 3600000 } ] } } ``` ```bash title="Migration Examples" theme={null} # v1: Update identity curl -X POST https://api.unkey.dev/v1/identities.updateIdentity \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"externalId": "user_123", "meta": {"plan": "enterprise"}, "ratelimits": [{"name": "requests", "limit": 5000, "duration": 3600000}]}' # v2: Update identity (enhanced flexibility) curl -X POST https://api.unkey.com/v2/identities.updateIdentity \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"identity": "user_123", "meta": {"plan": "enterprise"}, "ratelimits": [{"name": "requests", "limit": 5000, "duration": 3600000}]}' ``` *** ## POST /v1/identities.deleteIdentity → POST /v2/identities.deleteIdentity **Purpose:** Permanently delete an identity. **Changes:** Enhanced parameter flexibility (v2 accepts both ID types), structured response. ```json title="v1 vs v2: Delete Identity Request" expandable theme={null} // v1: Requires identityId specifically { "identityId": "id_abc123def456" } // v2: Flexible identity parameter { "identity": "user_123" // accepts both identityId or externalId } ``` ```json title="v1 vs v2: Delete Identity Response" expandable theme={null} // v1: Empty object response {} // v2: Structured response with meta wrapper { "meta": { "requestId": "req_deleteidentity999" } } ``` ```bash title="Migration Examples" theme={null} # v1: Delete identity (requires identityId) curl -X POST https://api.unkey.dev/v1/identities.deleteIdentity \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"identityId": "id_abc123def456"}' # v2: Delete identity (accepts externalId or identityId) curl -X POST https://api.unkey.com/v2/identities.deleteIdentity \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"identity": "user_123"}' ``` # Migration Guide Source: https://unkey.com/docs/api-reference/v1/migration/index Migrate from Unkey API v1 to v2 for enhanced features and improved infrastructure Unkey v2 represents a major infrastructure upgrade with enhanced caching and cache invalidation systems. While the core functionality remains the same, there are important changes to request and response structures that require updates to your integration. The v2 API is available at **api.unkey.com** (instead of api.unkey.dev). The v1 API is deprecated and will be shut down at the end of the year 2025. ## Quick Start The fastest way to migrate is to update your SDK: ```bash theme={null} npm install @unkey/api@latest ``` ```bash theme={null} pip install unkey.py@^2.0.4 ``` ```bash theme={null} go get github.com/unkeyed/sdks/api/go/v2 ``` The v2 SDK automatically points to the new endpoint and guides you through all request/response changes via its types. ## Why Migrate to v2? ### Proven Performance Improvements Our v2 changes produce measurable latency improvements across all regions: ### Key Changes of v2 * **New Domain:** API available at `api.unkey.com` instead of `api.unkey.dev` * **Enhanced Caching:** Server-based infrastructure with improved caching mechanisms * **Improved Invalidations:** Enhanced cache invalidation system across regions * **Improved Developer Experience:** More consistent error handling and response formats ## What You Need to Change The main changes you'll need to make when migrating: * **Update your base URL** from `api.unkey.dev` to `api.unkey.com` (The SDKs do this automatically starting at v2.0.0) * **Change response parsing** from direct responses to `response.data` * **Handle new error format** with hierarchical error codes and request IDs ## Response Format Changes All v2 responses now use a standardized envelope format with `meta` and `data` fields: #### v1 Response Format ```json theme={null} { "valid": true, "keyId": "key_123", "name": "Production API Key" } ``` #### v2 Response Format ```json theme={null} { "meta": { "requestId": "req_abc123" }, "data": { "valid": true, "keyId": "key_123", "name": "Production API Key" } } ``` *** ## Detailed Migration by Category Choose the endpoint category you need to migrate: Key creation, verification, updates, permissions, and roles API namespace creation, retrieval, and key listing Identity management and shared rate limits Standalone permission and role management Rate limiting and override management SQL-based analytics and verification data queries Error response format changes and codes ### Getting Help * **Documentation:** [https://unkey.com/docs/api-reference/v2](https://unkey.com/docs/api-reference/v2) * **Discord:** [https://unkey.com/discord](https://unkey.com/discord) * **GitHub Issues:** [https://github.com/unkeyed/unkey/issues](https://github.com/unkeyed/unkey/issues) * **Email Support:** [support@unkey.com](mailto:support@unkey.com) * **Book a Call:** [https://cal.com/team/unkey/founders](https://cal.com/team/unkey/founders) - Schedule time with our team for migration help or feedback # /v1/keys.* Source: https://unkey.com/docs/api-reference/v1/migration/keys Migrate key management endpoints from v1 to v2 This guide covers all key management endpoints including creation, verification, updates, permissions, and roles. ## Overview Key management endpoints are the core of the Unkey API, handling creation, verification, updates, permissions, and roles for API keys. ### Key Changes in v2: * **Response format**: Direct responses → `{meta, data}` envelope * **Owner ID**: `ownerId` field removed, use `externalId` only * **Credits**: `remaining` + `refill` → `credits` object * **Rate limits**: `ratelimit` object → `ratelimits` array * **Permission queries**: Object syntax → string syntax ### Migration Impact: * **Existing in v1**: Full key CRUD operations with permissions, roles, and rate limiting * **Enhanced in v2**: Improved response format, simplified field structures, and string-based queries * **Maintained in v2**: All core key management functionality with backward-compatible request formats *** ## POST /v1/keys.createKey → POST /v2/keys.createKey **Key Changes:** * Remove `ownerId` field, use `externalId` instead * Restructure `remaining` + `refill` → `credits` object * Change `ratelimit` object → `ratelimits` array * Response format: Direct response → `{meta, data}` envelope ```json Key Creation Request Diff expandable icon=key theme={null} { "apiId": "api_1234567890abcdef", "prefix": "prod", "name": "Production API Key", "ownerId": "user_456", // [!code --] "externalId": "customer_789", "permissions": ["documents.read", "documents.write"], "roles": ["editor"], "expires": 1735689600000, "remaining": 10000, // [!code --] "refill": { // [!code --] "interval": "monthly", // [!code --] "amount": 10000 // [!code --] }, // [!code --] "credits": { // [!code ++] "remaining": 10000, // [!code ++] "refill": { // [!code ++] "interval": "monthly", // [!code ++] "amount": 10000, // [!code ++] "refillDay": 1 // [!code ++] } // [!code ++] }, // [!code ++] "ratelimit": { // [!code --] "limit": 1000, // [!code --] "duration": 3600000, // [!code --] "async": true // [!code --] }, // [!code --] "ratelimits": [ // [!code ++] { // [!code ++] "name": "api_requests", // [!code ++] "limit": 1000, // [!code ++] "duration": 3600000, // [!code ++] "autoApply": true // [!code ++] } // [!code ++] ], // [!code ++] "enabled": true } ``` ```json Create Key Response Diff expandable icon=check-circle theme={null} // v1 Response (direct response) { "key": "sk_1234abcdef567890", // [!code --] "keyId": "key_abc123def456" // [!code --] } // v2 Response { "meta": { // [!code ++] "requestId": "req_xyz789abc123" // [!code ++] }, // [!code ++] "data": { // [!code ++] "key": "sk_1234abcdef567890", // [!code ++] "keyId": "key_abc123def456" // [!code ++] } // [!code ++] } ``` ```bash v1 cURL expandable icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.createKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_1234567890abcdef", "prefix": "prod", "name": "Production API Key", "ownerId": "user_456", "externalId": "customer_789", "permissions": ["documents.read", "documents.write"], "roles": ["editor"], "expires": 1735689600000, "remaining": 10000, "refill": { "interval": "monthly", "amount": 10000 }, "ratelimit": { "limit": 1000, "duration": 3600000, "async": true }, "enabled": true }' ``` ```bash v2 cURL expandable {4,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_1234567890abcdef", "prefix": "prod", "name": "Production API Key", "externalId": "customer_789", "permissions": ["documents.read", "documents.write"], "roles": ["editor"], "expires": 1735689600000, "credits": { "remaining": 10000, "refill": { "interval": "monthly", "amount": 10000, "refillDay": 1 } }, "ratelimits": [ { "name": "api_requests", "limit": 1000, "duration": 3600000, "autoApply": true } ], "enabled": true }' ``` *** ## POST /v1/keys.verifyKey → POST /v2/keys.verifyKey **Key Changes:** * **CRITICAL**: v2 requires root key authentication with `api.*.verify_key` permission * **CRITICAL**: `apiId` parameter is no longer accepted in v2 * Remove `authorization` wrapper for permissions * Use string-based permission queries instead of object syntax * Change `remaining` → `credits` for cost parameters * Add support for multiple named rate limits * Response format: Direct response → `{meta, data}` envelope **Action Required: Update Your Root Key Permissions** Your root key must have the `api..verify_key` permission for v2 key verification. [Learn more](#understanding-v2-root-key-permissions-for-key-verification) **Simple Key Verification** ```json Key Verification Request Changes icon=code theme={null} { "key": "sk_1234abcdef" } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.verifyKey \ -H "Content-Type: application/json" \ -d '{"key": "sk_1234abcdef"}' ``` ```bash v2 cURL {2} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"key": "sk_1234abcdef"}' ``` ```json Response Format Changes expandable icon=arrow-right theme={null} // v1 Response (direct response) { "valid": true, // [!code --] "code": "VALID", // [!code --] "keyId": "key_123", // [!code --] "name": "Production API Key", // [!code --] "ownerId": "user_456", // [!code --] "meta": { // [!code --] "roles": ["admin", "user"], // [!code --] "stripeCustomerId": "cus_1234" // [!code --] }, // [!code --] "expires": null, // [!code --] "remaining": 995, // [!code --] "permissions": ["documents.read"], // [!code --] "roles": ["editor"], // [!code --] "enabled": true, // [!code --] "environment": "production", // [!code --] "identity": { // [!code --] "id": "identity_123", // [!code --] "externalId": "customer_789", // [!code --] "meta": {} // [!code --] }, // [!code --] "requestId": "req_abc123" // [!code --] } // v2 Response { "meta": { // [!code ++] "requestId": "req_abc123" // [!code ++] }, // [!code ++] "data": { // [!code ++] "valid": true, // [!code ++] "code": "VALID", // [!code ++] "keyId": "key_123", // [!code ++] "credits": 995, // [!code ++] "expires": null, // [!code ++] "permissions": ["documents.read"], // [!code ++] "roles": ["editor"], // [!code ++] "identity": { // [!code ++] "id": "id_123", // [!code ++] "externalId": "customer_789", // [!code ++] "meta": {}, // [!code ++] "ratelimits": [] // [!code ++] }, // [!code ++] "ratelimits": [ // [!code ++] { // [!code ++] "id": "rl_123", // [!code ++] "name": "api_requests", // [!code ++] "limit": 1000, // [!code ++] "remaining": 999, // [!code ++] "reset": 1672531200000, // [!code ++] "exceeded": false, // [!code ++] "duration": 3600000, // [!code ++] "autoApply": true // [!code ++] } // [!code ++] ] // [!code ++] } // [!code ++] } ``` **Permission Verification** ```json Permission Query Syntax icon=shield theme={null} // v1 Request { "key": "sk_1234abcdef", "authorization": { // [!code --] "permissions": { // [!code --] "and": ["documents.read", "documents.write"] // [!code --] } // [!code --] } // [!code --] } // v2 Request { "key": "sk_1234abcdef", "permissions": "documents.read AND documents.write" // [!code ++] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.verifyKey \ -H "Content-Type: application/json" \ -d '{ "key": "sk_1234abcdef", "authorization": { "permissions": { "and": ["documents.read", "documents.write"] } } }' ``` ```bash v2 cURL {2,6} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "key": "sk_1234abcdef", "permissions": "documents.read AND documents.write" }' ``` **Credits and Rate Limits** ```json Credits & Rate Limits Structure icon=coins theme={null} // v1 Request { "key": "sk_1234abcdef", "remaining": { // [!code --] "cost": 5 // [!code --] } // [!code --] } // v2 Request { "key": "sk_1234abcdef", "credits": { // [!code ++] "cost": 5 // [!code ++] }, // [!code ++] "ratelimits": [ // [!code ++] { // [!code ++] "name": "heavy_operations", // [!code ++] "cost": 3 // [!code ++] } // [!code ++] ] // [!code ++] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.verifyKey \ -H "Content-Type: application/json" \ -d '{ "key": "sk_1234abcdef", "remaining": {"cost": 5} }' ``` ```bash v2 cURL {2,6,7,8,9,10} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "key": "sk_1234abcdef", "credits": {"cost": 5}, "ratelimits": [{ "name": "heavy_operations", "cost": 3 }] }' ``` *** ## Understanding v2 Root Key Permissions for Key Verification The v2 `keys.verifyKey` endpoint introduces a powerful permission system that gives you granular control over which services can verify keys from which APIs. ### Setting Up Root Key Permissions When creating a root key for key verification, you need to grant it the appropriate `verify_key` permission for your API: ```json Root Key with Permission for Specific API icon=shield theme={null} { "name": "Production API Verification Key", "permissions": [ { "name": "api.api_1234567890abcdef.verify_key", "description": "Allow verification of keys only from the Production API" } ] } ``` This root key can only verify keys from the specific API `api_1234567890abcdef`. Use this for services that should only authenticate users from a particular API. ```json Root Key with Permission for Multiple APIs icon=layers theme={null} { "name": "Multi-Service Verification Key", "permissions": [ { "name": "api.api_prod123.verify_key", "description": "Verify keys from Production API" }, { "name": "api.api_staging456.verify_key", "description": "Verify keys from Staging API" } ] } ``` This root key can verify keys from multiple specific APIs. Use this when you need to authenticate users from several APIs but not all APIs in the workspace. ```json Root Key with Permission to Verify Any API Key icon=key theme={null} { "name": "Service Authentication Key", "permissions": [ { "name": "api.*.verify_key", "description": "Allow verification of keys from any API in the workspace" } ] } ``` This root key can verify keys from any API in your workspace. Only use this if you specifically need to authenticate users across all APIs. ### Migration from v1 apiId Parameter In v1, you could specify which API's keys to verify using the `apiId` parameter: ```json v1: Explicit API Selection theme={null} { "key": "sk_1234abcdef", "apiId": "api_1234567890abcdef" // ❌ No longer supported in v2 } ``` In v2, this control is moved to the root key's permissions: ```json v2: Permission-Based API Selection theme={null} { "key": "sk_1234abcdef" // API access controlled by root key's api.*.verify_key permissions } ``` **Benefits of the New System:** * **Better Security**: Only authorized services can verify keys * **Granular Control**: Workspace owners control which services can verify keys from which APIs * **Simpler Integration**: No need to manage `apiId` parameters in your application code * **Audit Trail**: All key verifications are tied to specific root keys with known permissions *** ## GET /v1/keys.getKey → POST /v2/keys.getKey **Key Changes:** * HTTP method: GET → POST * Request body format required instead of query parameters * Response format: Direct response → `{meta, data}` envelope ```bash HTTP Method Change icon=arrow-right theme={null} # v1: GET with query parameters curl -X GET "https://api.unkey.dev/v1/keys.getKey?keyId=key_123" \ -H "Authorization: Bearer " # v2: POST with request body curl -X POST https://api.unkey.com/v2/keys.getKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"keyId": "key_123"}' ``` ```json Get Key Response Diff expandable icon=database theme={null} // v1 Response (direct response) { "id": "key_123", // [!code --] "start": "sk_5j1", // [!code --] "workspaceId": "ws_1234", // [!code --] "apiId": "api_abc", // [!code --] "name": "Production API Key", // [!code --] "ownerId": "user_456", // [!code --] "meta": { // [!code --] "roles": ["admin", "user"], // [!code --] "stripeCustomerId": "cus_1234" // [!code --] }, // [!code --] "createdAt": 1705306200000, // [!code --] "updatedAt": 1705306200000, // [!code --] "expires": null, // [!code --] "remaining": 995, // [!code --] "refill": { // [!code --] "interval": "monthly", // [!code --] "amount": 1000, // [!code --] "refillDay": 1, // [!code --] "lastRefillAt": 1705306200000 // [!code --] }, // [!code --] "ratelimit": { // [!code --] "async": true, // [!code --] "type": "fast", // [!code --] "limit": 100, // [!code --] "duration": 60000 // [!code --] }, // [!code --] "roles": ["admin", "finance"], // [!code --] "permissions": ["documents.read", "documents.write"], // [!code --] "enabled": true, // [!code --] "identity": { // [!code --] "id": "identity_123", // [!code --] "externalId": "customer_789", // [!code --] "meta": {} // [!code --] } // [!code --] } // v2 Response { "meta": { // [!code ++] "requestId": "req_xyz789" // [!code ++] }, // [!code ++] "data": { // [!code ++] "id": "key_123", // [!code ++] "name": "Production API Key", // [!code ++] "start": "prod_1234", // [!code ++] "meta": { // [!code ++] "plan": "enterprise" // [!code ++] }, // [!code ++] "createdAt": 1754304517, // [!code ++] "updatedAt": 1754304518, // [!code ++] "expires": 1764841705, // [!code ++] "credits": { // [!code ++] "remaining": 995, // [!code ++] "refill": { // [!code ++] "interval": "monthly", // [!code ++] "amount": 1000, // [!code ++] "refillDay": 1 // [!code ++] } // [!code ++] }, // [!code ++] "ratelimits": [ // [!code ++] { // [!code ++] "name": "api_requests", // [!code ++] "limit": 100, // [!code ++] "duration": 60000, // [!code ++] "autoApply": true // [!code ++] } // [!code ++] ], // [!code ++] "identity": { // [!code ++] "id": "id_123", // [!code ++] "externalId": "customer_789", // [!code ++] "meta": {}, // [!code ++] "ratelimits": [] // [!code ++] }, // [!code ++] "enabled": true, // [!code ++] "permissions": ["documents.read"], // [!code ++] "roles": ["editor"] // [!code ++] } // [!code ++] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X GET "https://api.unkey.dev/v1/keys.getKey?keyId=key_123" \ -H "Authorization: Bearer " ``` ```bash v2 cURL {1,3} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.getKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123" }' ``` *** ## POST /v1/keys.deleteKey → POST /v2/keys.deleteKey **Purpose:** Permanently delete an API key. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Added `permanent` parameter for hard deletion * Added `meta.requestId` for debugging ```json Delete Key Request expandable icon=trash theme={null} // v1 Request { "keyId": "key_123" } // v2 Request (enhanced) { "keyId": "key_123", "permanent": false // [!code ++] } ``` ```json Delete Key Response Diff icon=check-circle theme={null} // v1 Response (direct empty response) {} // [!code --] // v2 Response { "meta": { // [!code ++] "requestId": "req_deletekey789" // [!code ++] }, // [!code ++] "data": {} // [!code ++] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.deleteKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123" }' ``` ```bash v2 cURL {1,6} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.deleteKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "permanent": false }' ``` *** ## POST /v1/keys.updateKey → POST /v2/keys.updateKey **Purpose:** Update an existing API key's properties. **Key Changes:** * Same structural changes as `createKey` (credits, ratelimits, no ownerId) * Response format: Direct response → `{meta, data}` envelope * Support for partial updates ```json Update Key Request Diff expandable icon=edit theme={null} // v1 Request { "keyId": "key_123", "name": "Updated Production Key", "ownerId": "user_456", // [!code --] "remaining": 5000, // [!code --] "ratelimit": { // [!code --] "limit": 2000, // [!code --] "duration": 3600000 // [!code --] } // [!code --] } // v2 Request { "keyId": "key_123", "name": "Updated Production Key", "externalId": "user_456", // [!code ++] "credits": { // [!code ++] "remaining": 5000 // [!code ++] }, // [!code ++] "ratelimits": [ // [!code ++] { // [!code ++] "name": "api_requests", // [!code ++] "limit": 2000, // [!code ++] "duration": 3600000 // [!code ++] } // [!code ++] ] // [!code ++] } ``` ```json Update Key Response Diff icon=check-circle theme={null} // v1 Response (direct empty response) {} // [!code --] // v2 Response { "meta": { // [!code ++] "requestId": "req_updatekey456" // [!code ++] }, // [!code ++] "data": {} } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.updateKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "name": "Updated Production Key", "ownerId": "user_456", "remaining": 5000 }' ``` ```bash v2 cURL {1,6,7,8} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "name": "Updated Production Key", "externalId": "user_456", "credits": { "remaining": 5000 } }' ``` *** ## POST /v1/keys.updateRemaining → POST /v2/keys.updateCredits **Purpose:** Update the credit/usage count for an API key. **Key Changes:** * Endpoint name change: `updateRemaining` → `updateCredits` * Request changes: `op` → `operation` * Response format: Direct response → `{meta, data}` envelope ```json Update Credits Request Diff expandable icon=coins theme={null} // v1 Request { "keyId": "key_123", "op": "set", // [!code --] "value": 1000 } // v2 Request { "keyId": "key_123", "operation": "set", // [!code ++] "value": 1000 } ``` ```json Update Credits Response Diff expandable icon=check-circle theme={null} // v1 Response (direct response) { "remaining": 1000 // [!code --] } // v2 Response { "meta": { // [!code ++] "requestId": "req_updatecredits123" // [!code ++] }, // [!code ++] "data": { // [!code ++] "credits": { // [!code ++] "remaining": 1000 // [!code ++] } // [!code ++] } // [!code ++] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.updateRemaining \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "value": 1000 }' ``` ```bash v2 cURL {1,6} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.updateCredits \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "operation": "set", "value": 1000 }' ``` *** ## POST /v1/keys.whoami → POST /v2/keys.whoami **Purpose:** Get information about the current API key being used. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Enhanced response with additional metadata, mirroring the `v2/keys.getKey` response. * Added `meta.requestId` for debugging ```json Whoami Request icon=user theme={null} // v1 & v2 Request (unchanged) { "key": "your_api_key_here" } ``` ```json Whoami Response Diff expandable icon=database theme={null} // v1 Response (direct response) { "id": "key_123", // [!code --] "name": "Production API Key", // [!code --] "remaining": 1000, // [!code --] "identity": { // [!code --] "id": "id_123", // [!code --] "externalId": "ext123" // [!code --] }, // [!code --] "meta": { // [!code --] "role": "admin", // [!code --] "plan": "premium" // [!code --] }, // [!code --] "createdAt": 1620000000000, // [!code --] "enabled": true, // [!code --] "environment": "production" // [!code --] } // v2 Response { "meta": { // [!code ++] "requestId": "req_whoami789" // [!code ++] }, // [!code ++] "data": { // [!code ++] "keyId": "key_123", // [!code ++] "name": "Production API Key", // [!code ++] "permissions": ["documents.read"], // [!code ++] "roles": ["editor"], // [!code ++] "identity": { // [!code ++] "id": "id_123", // [!code ++] "externalId": "customer_789", // [!code ++] "meta": {}, // [!code ++] "ratelimits": [] // [!code ++] }, // [!code ++] "createdAt": 1754304517 // [!code ++] } // [!code ++] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.whoami \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "key": "some_api_key" }' ``` ```bash v2 cURL {1} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.whoami \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "key": "some_api_key" }' ``` *** ## New: /v2/keys.rerollKey **Purpose:** Generate a new API key while preserving the configuration from an existing key.\ With this endpoint, you no longer have to delete an old key and recreate it if a key is unrecoverable and you don't know the plaintext key anymore. **Important:** Analytics and usage metrics are tracked at both the key level AND identity level. \ If the original key has an identity, the new key will inherit it, allowing you to track usage across both individual keys and the overall identity.\ The new key will NOT inherit the usage from the old keyId. ```json theme={null} { "keyId": "key_123", "expiration": 0 // This expires the key directly } ``` ```json expandable theme={null} { "key": "sk_1234abcdef5678", "keyId": "key_456" } ``` ## Permission Management Endpoints ### POST /v1/keys.addPermissions → POST /v2/keys.addPermissions **Purpose:** Add permissions to an existing API key. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Will auto create permissions if they don't exist, and the root key has the `rbac.*.create_permission` permission. Otherwise it will return a permission error. * Only identify permission by their slugs. * Added `meta.requestId` for debugging ```json Add Permissions Request expandable icon=shield-plus theme={null} // v1 request { "keyId": "key_123", "permissions": [ { // [!code --] "id": "perm_123", // [!code --] "create": true // [!code --] }, // [!code --] { // [!code --] "name": "documents.read", // [!code --] } // [!code --] ] } // v2 request { "keyId": "key_123", "permissions": ["documents.read", "documents.write"] // [!code ++] } ``` ```json Add Permissions Response Diff icon=check-circle theme={null} // v1 Response (array of attached permissions) [ { "id": "perm_123", "name": "documents.read" }, { "id": "perm_456", "name": "documents.write" } ] // v2 Response, responds with the permissions that are attached to this key { "meta": { // [!code ++] "requestId": "req_addperms123" // [!code ++] }, // [!code ++] "data": [{ "id": "perm_123", "name" : "Read Documents", "slug" : "documents.read", // [!code ++] "description": "Read documents but don't write them", // [!code ++] }] // [!code ++] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.addPermissions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "permissions": [ { "name": "documents.read", "description": "Read access to documents" } ] }' ``` ```bash v2 cURL {1} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.addPermissions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "permissions": ["documents.read"] }' ``` *** ### POST /v1/keys.removePermissions → POST /v2/keys.removePermissions **Purpose:** Remove permissions from an existing API key. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Only accepts permission slugs now * Added `meta.requestId` for debugging ```json Remove Permissions Request icon=shield-minus theme={null} // v1 Request { "keyId": "key_123", "permissions": [ { // [!code --] "id": "perm_123" // [!code --] }, // [!code --] { // [!code --] "slug": "description.read" // [!code --] }, // [!code --] ] } // v2 Request { "keyId": "key_123", "permissions": ["documents.write", "documents.delete"] // [!code ++] } ``` ```json Remove Permissions Response Diff icon=check-circle theme={null} // v1 Response (direct empty response) {} // [!code --] // v2 Response, responds with the remaining permissions { "meta": { // [!code ++] "requestId": "req_removeperms456" // [!code ++] }, // [!code ++] "data": [ // [!code ++] { // [!code ++] "id": "perm_123", // [!code ++] "slug": "documents.archive", // [!code ++] "name": "Documents Archive", // [!code ++] "description": "Allows archiving documents" // [!code ++] }, // [!code ++] ] // [!code ++] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.removePermissions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "permissions": [{ "slug": "documents.write", }] }' ``` ```bash v2 cURL {1} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.removePermissions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "permissions": ["documents.write", "documents.delete"] }' ``` *** ### POST /v1/keys.setPermissions → POST /v2/keys.setPermissions **Purpose:** Atomically replace all permissions on an API key. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Atomic replacement of all permissions * Will auto create permissions if they don't exist, and the root key has the `rbac.*.create_permission` permission. Otherwise it will return a permission error. * Added `meta.requestId` for debugging ```json Set Permissions Request expandable icon=shield-check theme={null} // v1 Request { "keyId": "key_123", "permissions": [ { // [!code --] "slug": "documents.read", // [!code --] "create": true, // [!code --] }, // [!code --] { // [!code --] "id": "perm_123", // [!code --] } // [!code --] ] } // v2 Request { "keyId": "key_123", "permissions": ["documents.read", "comments.moderate"] // [!code ++] } ``` ```json Set Permissions Response Diff icon=check-circle theme={null} // v1 Response (array of all permissions on key) [ { "id": "perm_123", // [!code --] "name": "documents.read" // [!code --] }, { "id": "perm_789", // [!code --] "name": "comments.moderate" // [!code --] } ] // [!code --] // v2 Response { "meta": { // [!code ++] "requestId": "req_setperms789" // [!code ++] }, // [!code ++] "data": [{ // [!code ++] "id": "perm_123", // [!code ++] "slug": "documents.read", // [!code ++] "name": "Documents Read", // [!code ++] "description": "Read access to documents" // [!code ++] }, // [!code ++] { // [!code ++] "id": "perm_789", // [!code ++] "slug": "comments.moderate", // [!code ++] "name": "Comments Moderate", // [!code ++] "description": "Moderate comments" // [!code ++] }] // [!code ++] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.setPermissions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "permissions": [ { "slug": "documents.read", } ] }' ``` ```bash v2 cURL {1} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.setPermissions \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "permissions": ["documents.read"] }' ``` *** ## Role Management Endpoints ### POST /v1/keys.addRoles → POST /v2/keys.addRoles **Purpose:** Add roles to an existing API key. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * No option to auto-create roles if they don't exist * Responds with all the roles that are attached to the key * Added `meta.requestId` for debugging ```json Add Roles Request icon=user-plus theme={null} // v1 Request { "keyId": "key_123", "roles": [ { "name": "editor" }, // [!code --] { "id": "role_123" } // [!code --] ] } // v2 Request { "keyId": "key_123", "roles": ["editor", "moderator"] // [!code ++] } ``` ```json Add Roles Response Diff icon=check-circle theme={null} // v1 Response (array of added roles) [ { "id": "role_123", "name": "editor" }, { "id": "role_456", "name": "moderator" } ] // v2 Response { "meta": { // [!code ++] "requestId": "req_addroles123" // [!code ++] }, // [!code ++] "data": [ { "id": "role_123", "name": "editor", "permissions": [{ // [!code ++] "id": "perm_123", // [!code ++] "name": "document reader", // [!code ++] "slug": "document.read", // [!code ++] "description": "Read documents but don't edit them" // [!code ++] }] // [!code ++] }, { "id": "role_456", "name": "moderator" } ] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.addRoles \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "roles": [ { "id": "role_123" }, { "name": "editor" } ] }' ``` ```bash v2 cURL {1} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.addRoles \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "roles": ["editor", "moderator"] }' ``` *** ### POST /v1/keys.removeRoles → POST /v2/keys.removeRoles **Purpose:** Remove roles from an existing API key. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Responds with the remaining roles on the key * Added `meta.requestId` for debugging ```json Remove Roles Request icon=user-minus theme={null} // v1 Request { "keyId": "key_123", "roles": [ { "name": "editor" }, // [!code --] { "id": "role_123" } // [!code --] ] } // v2 Request { "keyId": "key_123", "roles": ["editor", "moderator"] // [!code ++] } ``` ```json Remove Roles Response Diff icon=check-circle theme={null} // v1 Response (direct empty response) {} // [!code --] // v2 Response { "meta": { // [!code ++] "requestId": "req_removeroles456" // [!code ++] }, // [!code ++] "data": [] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.removeRoles \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "roles": ["moderator"] }' ``` ```bash v2 cURL {1} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.removeRoles \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "roles": ["moderator"] }' ``` *** ### POST /v1/keys.setRoles → POST /v2/keys.setRoles **Purpose:** Atomically replace all roles on an API key. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Atomic replacement of all roles * Responds with all the roles that are now set * No auto creation of roles anymore * Added `meta.requestId` for debugging ```json Set Roles Request icon=users-cog theme={null} // v1 Request { "keyId": "key_123", "roles": [ { "name": "editor" }, // [!code --] { "id": "role_123" } // [!code --] ] } // v2 Request { "keyId": "key_123", "roles": ["editor", "moderator"] // [!code ++] } ``` ```json Set Roles Response Diff icon=check-circle theme={null} // v1 Response (array of all roles on key) [ { "id": "role_123", "name": "editor" }, { "id": "role_789", "name": "moderator" } ] // v2 Response { "meta": { // [!code ++] "requestId": "req_setroles789" // [!code ++] }, // [!code ++] "data": [ { "id": "role_123", "name": "editor", "permissions": [ // [!code ++] { // [!code ++] "id": "perm_123", // [!code ++] "name": "document reader", // [!code ++] "slug": "document.read", // [!code ++] "description": "Read documents but don't edit them" // [!code ++] } ] }, { "id": "role_456", "name": "moderator" } ] } ``` ```bash v1 cURL icon=terminal theme={null} curl -X POST https://api.unkey.dev/v1/keys.setRoles \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "roles": [ { "name": "editor" }, { "id": "role_789" } ] }' ``` ```bash v2 cURL {1} icon=terminal theme={null} curl -X POST https://api.unkey.com/v2/keys.setRoles \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_123", "roles": ["editor", "admin"] }' ``` *** ## Migration Patterns ### Response Format Migration ```typescript v1 vs v2: Response Handling theme={null} // v1: Access data directly const key = await fetch('/v1/keys.getKey', { // [!code --] method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ keyId: 'key_123' }) }); const data = await key.json(); // [!code --] const keyData = data; // v1 direct format // [!code --] console.log(keyData.keyId); // v2: Access data through data field const key = await fetch('/v2/keys.getKey', { // [!code ++] method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ keyId: 'key_123' }) }); const response = await key.json(); // [!code ++] const keyData = response.data; // v2 format // [!code ++] const requestId = response.meta.requestId; // for debugging // [!code ++] console.log(keyData.keyId); ``` ### Key Structure Migration ```json v1 vs v2: Key Structure theme={null} // v1 Key Structure { "apiId": "api_123", "ownerId": "user_456", // [!code --] "remaining": 1000, // [!code --] "refill": { // [!code --] "interval": "monthly", // [!code --] "amount": 1000 // [!code --] }, // [!code --] "ratelimit": { // [!code --] "limit": 100, // [!code --] "duration": 60000, // [!code --] "async": true // [!code --] } // [!code --] } // v2 Key Structure { "identity": { // [!code ++] "externalId": "user_456", // [!code ++] "id": "id_123", // [!code ++] "ratelimits": [], // [!code ++] "meta": {}, // [!code ++] }, // [!code ++] "credits": { // [!code ++] "remaining": 1000, // [!code ++] "refill": { // [!code ++] "interval": "monthly", // [!code ++] "amount": 1000, // [!code ++] "refillDay": 1 // [!code ++] } // [!code ++] }, // [!code ++] "ratelimits": [ // [!code ++] { // [!code ++] "name": "api_requests", // [!code ++] "limit": 100, // [!code ++] "duration": 60000, // [!code ++] "autoApply": true // [!code ++] } // [!code ++] ] // [!code ++] } ``` *** ## Migration Checklist ### Key Creation & Updates * Replace `ownerId` with `externalId` * Update `remaining` + `refill` → `credits` structure * Convert `ratelimit` → `ratelimits` array * Add `name` field to rate limits * Change `async` parameter to `autoApply` * Add `refillDay` for monthly intervals ### Key Verification * **CRITICAL**: Create root key with `api.*.verify_key` permission for your verification service * Add root key authentication header to all key verification calls * Remove `apiId` parameter from verification requests (controlled by root key permissions now) * Convert permission query objects to strings: `"perm1 AND perm2"` * Update `remaining` → `credits` for cost parameters * Handle new rate limits array structure in responses * Test verification with both wildcard (`api.*.verify_key`) and specific API permissions ### Response Handling * Change `response` (direct) to `response.data` in all key operations * Extract and log `meta.requestId` from responses for debugging * Remove references to `ownerId` in response parsing * Update error handling for new response structure ### Endpoint Updates * Update `keys.updateRemaining` → `keys.updateCredits` * Add `operation` parameter for credit updates (set/increment/decrement) * Add `permanent` parameter for key deletion if needed ### Testing * Test key creation with new structure * Test key verification with string-based permission queries * Test permission and role management operations * Verify key updates work with new credit structure * Confirm all responses follow new envelope format # /v1/permissions.* Source: https://unkey.com/docs/api-reference/v1/migration/permissions Migrate from key-based permission patterns to dedicated permission and role management in v2 This guide covers migrating from v1's key-based permission patterns to v2's dedicated RBAC (Role-Based Access Control) system. ## Overview Both v1 and v2 have standalone permission and role management endpoints. The main differences are in request/response formats, HTTP methods, and enhanced functionality in v2. ### Key Changes in v2: * **Response format**: Direct responses → `{meta, data}` envelope * **HTTP methods**: Some GET → POST changes for consistency * **Enhanced pagination**: Better pagination support in list endpoints * **Domain change**: `api.unkey.dev` → `api.unkey.com` ### Migration Impact: * **Both versions**: Have standalone permission and role endpoints * **v2 Enhancement**: Improved response format, consistent HTTP methods, simplified permission queries * **Benefit**: Better API consistency, enhanced debugging with request IDs, simplified permission syntax *** ## POST /v1/permissions.createPermission → POST /v2/permissions.createPermission **Purpose:** Create a standalone permission that can be reused across keys and roles. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Domain change: `api.unkey.dev` → `api.unkey.com` ```bash title="v1 vs v2: Standalone Permission Creation" icon="shield-plus" theme={null} # v1: Create permission independently curl -X POST https://api.unkey.dev/v1/permissions.createPermission # [!code --] -H "Authorization: Bearer " # [!code --] -H "Content-Type: application/json" -d '{"name": "documents.read", "description": "Read access to documents"}' # [!code --] # v2: Create permission once, reuse everywhere curl -X POST https://api.unkey.com/v2/permissions.createPermission # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"slug": "documents.read", "name": "documents.read", "description": "Read access to documents"}' # [!code ++] ``` ```json title="Create Permission Response" icon="check-circle" expandable theme={null} { "meta": { "requestId": "req_createpermission123" }, "data": { "id": "perm_abc123def456", "name": "documents.read", "slug": "documents.read", "description": "Read access to documents" } } ``` *** ## GET /v1/permissions.getPermission → POST /v2/permissions.getPermission **Purpose:** Retrieve permission details by name. **Key Changes:** * HTTP method: GET → POST * Request format: Query parameters → Request body * Response format: Direct response → `{meta, data}` envelope * Lookup Permission by either ID or Slug ```bash title="v1 vs v2: Direct Permission Retrieval" icon="shield" theme={null} # v1: Get permission by ID curl -X GET "https://api.unkey.dev/v1/permissions.getPermission?permissionId=perm_123" # [!code --] -H "Authorization: Bearer " # [!code --] # v2: Direct permission lookup curl -X POST https://api.unkey.com/v2/permissions.getPermission # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"permission": "documents.read"}' # [!code ++] ``` ```json title="Get Permission Response" icon="database" expandable theme={null} { "meta": { "requestId": "req_getpermission456" }, "data": { "id": "perm_abc123def456", "name": "documents.read", "slug": "documents.read", "description": "Read access to documents" } } ``` *** ## GET /v1/permissions.listPermissions → POST /v2/permissions.listPermissions **Purpose:** Get paginated list of all permissions. **Key Changes:** * HTTP method: GET → POST * Request format: Query parameters → Request body * Response format: Direct array → `{meta, data}` envelope with pagination ```bash title="v1 vs v2: Direct Permission Listing" icon="shield-check" theme={null} # v1: List all permissions directly curl -X GET "https://api.unkey.dev/v1/permissions.listPermissions" # [!code --] -H "Authorization: Bearer " # [!code --] # v2: Direct permission listing with pagination curl -X POST https://api.unkey.com/v2/permissions.listPermissions # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"limit": 100, "cursor": "optional_cursor"}' # [!code ++] ``` ```json title="List Permissions Response" icon="list" expandable theme={null} { "meta": { "requestId": "req_listpermissions789" }, "data": [ { "permissionId": "perm_abc123", "name": "Read documents", "slug": "documents.read", "description": "Read access to documents", }, { "permissionId": "perm_def456", "name": "Write documents", "slug": "documents.write", "description": "Write access to documents", } ], "pagination": { "cursor": "next_page_cursor_here", "hasMore": true } } ``` *** ## POST /v1/permissions.deletePermission → POST /v2/permissions.deletePermission **Purpose:** Permanently delete a permission globally. **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Delete permission by ID or slug * Domain change: `api.unkey.dev` → `api.unkey.com` ```bash title="v1 vs v2: Global Permission Deletion" icon="shield-minus" theme={null} # v1: Delete permission globally curl -X POST https://api.unkey.dev/v1/permissions.deletePermission # [!code --] -H "Authorization: Bearer " # [!code --] -H "Content-Type: application/json" -d '{"permissionId": "perm_123"}' # [!code --] # v2: Delete permission globally (removes from all keys and roles) curl -X POST https://api.unkey.com/v2/permissions.deletePermission # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"permission": "documents.read"}' # [!code ++] ``` ```json title="Delete Permission Response" icon="check-circle" theme={null} { "meta": { "requestId": "req_deletepermission999" }, "data": {} } ``` *** ## Role-Based Access Control (RBAC) Migration **Purpose:** Group permissions into roles for easier management - available in both v1 and v2. **Key Changes:** * Response format: Direct responses → `{meta, data}` envelope * Enhanced role listing with better pagination in v2 ### POST /v1/permissions.createRole → POST /v2/permissions.createRole ```bash title="v1 vs v2: Role Creation" icon="users-cog" theme={null} # v1: Create role curl -X POST https://api.unkey.dev/v1/permissions.createRole # [!code --] -H "Authorization: Bearer " # [!code --] -H "Content-Type: application/json" -d '{"name": "editor", "description": "Content editor role"}' # [!code --] # v2: Create role (envelope response) curl -X POST https://api.unkey.com/v2/permissions.createRole # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"name": "editor", "description": "Content editor role"}' # [!code ++] ``` ```json title="Create Role Response" icon="check-circle" expandable theme={null} { "meta": { "requestId": "req_createrole123" }, "data": { "roleId": "role_abc123def456" } } ``` ```bash title="Assign Role to Key" icon="key" theme={null} # Assign role to key (gives all role permissions) curl -X POST https://api.unkey.com/v2/keys.addRoles # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"keyId": "key_123", "roles": ["editor"]}' # [!code ++] ``` ### GET /v1/permissions.getRole → POST /v2/permissions.getRole * Lookup role by either ID or name ```bash title="v1 vs v2: Role Retrieval" icon="user" theme={null} # v1: GET with query parameter curl -X GET "https://api.unkey.dev/v1/permissions.getRole?roleId=role_123" # [!code --] -H "Authorization: Bearer " # [!code --] # v2: POST with request body curl -X POST https://api.unkey.com/v2/permissions.getRole # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"role": "editor"}' # [!code ++] ``` ```json title="Get Role Response" icon="database" expandable theme={null} { "meta": { "requestId": "req_getrole456" }, "data": { "id": "role_abc123def456", "name": "editor", "description": "Content editor role", "permissions": [ "documents.read", "documents.write", "comments.moderate" ] } } ``` ### GET /v1/permissions.listRoles → POST /v2/permissions.listRoles ```bash title="v1 vs v2: Role Listing" icon="user-group" theme={null} # v1: GET request curl -X GET "https://api.unkey.dev/v1/permissions.listRoles" # [!code --] -H "Authorization: Bearer " # [!code --] # v2: POST with enhanced pagination curl -X POST https://api.unkey.com/v2/permissions.listRoles # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"limit": 100, "cursor": "optional_cursor"}' # [!code ++] ``` ```json title="List Roles Response" icon="list" expandable theme={null} { "meta": { "requestId": "req_listroles789" }, "data": [ { "roleId": "role_abc123", "name": "editor", "description": "Content editor role", "permissions": [ "documents.read", "documents.write", "comments.moderate" ] }, { "roleId": "role_def456", "name": "admin", "description": "Full administrative access", "permissions": [ "documents.*", "users.*", "settings.*" ] } ], "pagination": { "cursor": "next_page_cursor_here", "hasMore": true } } ``` ### POST /v1/permissions.deleteRole → POST /v2/permissions.deleteRole * Delete role by either ID or name ```bash title="v1 vs v2: Role Deletion" icon="user-minus" theme={null} # v1: Delete role globally curl -X POST https://api.unkey.dev/v1/permissions.deleteRole # [!code --] -H "Authorization: Bearer " # [!code --] -H "Content-Type: application/json" -d '{"roleId": "role_123"}' # [!code --] # v2: Delete role globally (envelope response) curl -X POST https://api.unkey.com/v2/permissions.deleteRole # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"role": "editor"}' # [!code ++] ``` ```json title="Delete Role Response" icon="check-circle" theme={null} { "meta": { "requestId": "req_deleterole999" }, "data": {} } ``` *** ## Permission Query Migration: v1 Object → v2 String Syntax **Key Change:** v2 simplifies permission queries from complex object syntax to intuitive string syntax. ```json title="AND Query Migration" icon="arrow-right" theme={null} // v1: Object syntax for key verification { "authorization": { // [!code --] "permissions": { // [!code --] "and": ["documents.read", "documents.write"] // [!code --] } // [!code --] } // [!code --] } // v2: String syntax for key verification { "permissions": "documents.read AND documents.write" // [!code ++] } ``` ```json title="OR Query Migration" icon="arrow-right" theme={null} // v1: Object syntax { "authorization": { // [!code --] "permissions": { // [!code --] "or": ["documents.read", "documents.write"] // [!code --] } // [!code --] } // [!code --] } // v2: String syntax { "permissions": "documents.read OR documents.write" // [!code ++] } ``` ```json title="Complex Query Migration" icon="arrow-right" theme={null} // v1: Nested object syntax { "authorization": { // [!code --] "permissions": { // [!code --] "and": [ // [!code --] "documents.read", // [!code --] { // [!code --] "or": ["documents.write", "documents.delete"] // [!code --] } // [!code --] ] // [!code --] } // [!code --] } // [!code --] } // v2: String syntax with parentheses { "permissions": "documents.read AND (documents.write OR documents.delete)" // [!code ++] } ``` ```bash title="Permission Query in Key Verification" icon="terminal" theme={null} # v1: Complex object syntax curl -X POST https://api.unkey.dev/v1/keys.verifyKey # [!code --] -H "Authorization: Bearer " # [!code --] -H "Content-Type: application/json" -d '{"key": "uk_123", "authorization": {"permissions": {"and": ["documents.read", "documents.write"]}}}' # [!code --] # v2: Simple string syntax curl -X POST https://api.unkey.com/v2/keys.verifyKey # [!code ++] -H "Authorization: Bearer " # [!code ++] -H "Content-Type: application/json" -d '{"key": "uk_123", "permissions": "documents.read AND documents.write"}' # [!code ++] ``` *** ## Migration Patterns ### Response Format Migration ```typescript title="v1 vs v2: Standalone Permission Management" theme={null} // v1: Create permission independently const response = await fetch('/v1/permissions.createPermission', { // [!code --] method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'documents.read', description: 'Read access to documents' }) }); const permissionData = await response.json(); // Direct response // [!code --] // Permission exists independently, can be reused // v2: Create permission independently const permissionResponse = await fetch('/v2/permissions.createPermission', { // [!code ++] method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'documents.read', description: 'Read access to documents' }) }); const result = await permissionResponse.json(); // [!code ++] const permissionData = result.data; // v2 envelope format // [!code ++] const requestId = result.meta.requestId; // For debugging // [!code ++] // Permission now exists independently, can be reused ``` ### RBAC Implementation Patterns ```typescript title="v1 vs v2: RBAC Implementation Patterns" theme={null} // v1: Had to add permissions to each key individually const keys = ['key_123', 'key_456', 'key_789']; const permissions = [ // [!code --] { name: 'documents.read', description: 'Read access' }, // [!code --] { name: 'documents.write', description: 'Write access' } // [!code --] ]; // [!code --] // Add same permissions to multiple keys // [!code --] for (const keyId of keys) { // [!code --] await fetch('/v1/keys.addPermissions', { // [!code --] method: 'POST', // [!code --] headers: { // [!code --] 'Authorization': 'Bearer ', // [!code --] 'Content-Type': 'application/json' // [!code --] }, // [!code --] body: JSON.stringify({ keyId, permissions }) // [!code --] }); // [!code --] } // [!code --] // v2: Create role once, assign to multiple keys const roleResponse = await fetch('/v2/permissions.createRole', { // [!code ++] method: 'POST', // [!code ++] headers: { // [!code ++] 'Authorization': 'Bearer ', // [!code ++] 'Content-Type': 'application/json' // [!code ++] }, // [!code ++] body: JSON.stringify({ // [!code ++] name: 'editor', // [!code ++] description: 'Content editor role', // [!code ++] permissions: ['documents.read', 'documents.write', 'comments.moderate'] // [!code ++] }) // [!code ++] }); // [!code ++] // Assign role to multiple keys (much more efficient) // [!code ++] for (const keyId of keys) { // [!code ++] await fetch('/v2/keys.addRoles', { // [!code ++] method: 'POST', // [!code ++] headers: { // [!code ++] 'Authorization': 'Bearer ', // [!code ++] 'Content-Type': 'application/json' // [!code ++] }, // [!code ++] body: JSON.stringify({ keyId, roles: ['editor'] }) // [!code ++] }); // [!code ++] } // [!code ++] ``` *** ## Key Benefits of v2 Permission Management ### Reusable Permission Definitions ```json title="Standalone permission creation" icon="shield-plus" theme={null} { "name": "api.execute", "description": "Execute API operations" } ``` Create permissions once, use across multiple keys and roles. ### Role-Based Access Control ```json title="Role with grouped permissions" icon="users-cog" theme={null} { "name": "api_admin", "description": "Full API administrative access", "permissions": [ "api.execute", "api.read", "api.write", "api.delete", "users.manage" ] } ``` Group related permissions into roles for easier management. ### Auto-Creation Support ```json title="Auto-create when referenced" icon="magic-wand" theme={null} { "keyId": "key_123", "permissions": ["new.permission"] } ``` Permissions and roles are automatically created when referenced if they don't exist. ### Simplified Query Syntax ```bash title="String-based permission queries" icon="code" theme={null} # Simple AND "permissions": "read AND write" # Simple OR "permissions": "read OR write" # Complex with parentheses "permissions": "read AND (write OR delete)" ``` ## Migration Checklist ### Pattern Migration * [ ] Identify current v1 permission and role usage patterns * [ ] Update HTTP methods (GET → POST for some endpoints) * [ ] Update request formats (query parameters → request body) * [ ] Update response parsing (direct → envelope format) ### Enhanced Functionality * [ ] Update to v2 envelope response format with `meta.requestId` * [ ] Use enhanced pagination in list endpoints * [ ] Update domain from `api.unkey.dev` to `api.unkey.com` * [ ] Leverage auto-creation for dynamic permission scenarios ### Query Syntax Migration * [ ] Convert object-based permission queries to string syntax in key verification * [ ] Update AND operations: `{"and": []}` → `"perm1 AND perm2"` * [ ] Update OR operations: `{"or": []}` → `"perm1 OR perm2"` * [ ] Handle complex nested queries with parentheses: `"perm1 AND (perm2 OR perm3)"` ### Response Format Updates * [ ] Update response parsing from direct format to `response.data` * [ ] Extract and log `meta.requestId` from responses for debugging * [ ] Handle new error structure with meta envelope ### Testing * [ ] Test HTTP method changes (GET → POST) * [ ] Verify request body format vs query parameters * [ ] Test permission queries with new string syntax * [ ] Confirm envelope response format parsing # /v1/ratelimits.* Source: https://unkey.com/docs/api-reference/v1/migration/ratelimiting Migrate rate limiting and override management endpoints from v1 to v2 This guide covers rate limiting functionality including namespace creation, override management, and rate limit checking. ## Overview Rate limiting endpoints manage request limits, overrides, and namespace-based rate limiting across your API infrastructure. ### Key Changes in v2: * **Response format**: `result` → `{meta, data}` wrapper * **Rate limit structure**: Single `ratelimit` object → `ratelimits` array with named limits * **Override management**: Enhanced override response format with additional metadata * **Async handling**: Removed `async` parameter * **Auto apply**: Added `autoApply` parameter to apply ratelimits to key verifications automatically * **Resources**: Removed `resources` array * **Metadata**: Removed `meta` object from request body ### Migration Impact: * **Existing in v1**: Full rate limiting and override management functionality * **Enhanced in v2**: Improved response format, better override metadata, and new listing capabilities * **Maintained in v2**: All core rate limiting functionality with backward-compatible request formats *** ## Rate Limit Checking ### POST /v1/ratelimits.limit → POST /v2/ratelimits.limit **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Enhanced response with additional metadata * Better override handling ```json title="Rate Limit Request" icon="bolt" theme={null} { "namespace": "email_sending", "identifier": "user_123", "limit": 100, "duration": 3600000, "cost": 1, "async": false, // [!code --] "meta": {}, // [!code --] "resources": [ // [!code --] { // [!code --] "type": "project", // [!code --] "id": "p_123", // [!code --] "name": "unkey" // [!code --] } // [!code --] ] // [!code --] } ``` ```json title="Rate Limit Response Diff" icon="database" expandable theme={null} // v1 Response (direct response) { "success": true, // [!code --] "limit": 100, // [!code --] "remaining": 99, // [!code --] "reset": 1672531200000 // [!code --] } // v2 Response { "meta": { // [!code ++] "requestId": "req_ratelimit123" // [!code ++] }, // [!code ++] "data": { // [!code ++] "success": true, // [!code ++] "limit": 100, // [!code ++] "remaining": 99, // [!code ++] "reset": 1672531200000, // [!code ++] "overrideId": "rlor_123" // [!code ++] } // [!code ++] } ``` ```bash title="v1 cURL" icon="terminal" theme={null} curl -X POST https://api.unkey.dev/v1/ratelimits.limit \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "namespace": "email_sending", "identifier": "user_123", "limit": 100, "duration": 3600000, "cost": 1 }' ``` ```bash title="v2 cURL" icon="terminal" theme={null} curl -X POST https://api.unkey.com/v2/ratelimits.limit \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "namespace": "email_sending", "identifier": "user_123", "limit": 100, "duration": 3600000, "cost": 1 }' ``` *** ## Rate Limit Overrides ### POST /v1/ratelimits.setOverride → POST /v2/ratelimits.setOverride **Key Changes:** * Response format: Direct response → `{meta, data}` envelope * Enhanced override targeting options * Better validation and error handling ```json title="Set Override Request" icon="settings" expandable theme={null} { "namespace": "api_requests", "identifier": "premium_user_456", "limit": 10000, "duration": 3600000, "async": false } ``` ```json title="Set Override Response Diff" icon="check-circle" expandable theme={null} // v1 Response (direct response) { "overrideId": "rlor_123" // [!code --] } // v2 Response { "meta": { // [!code ++] "requestId": "req_setoverride456" // [!code ++] }, // [!code ++] "data": { // [!code ++] "overrideId": "rlor_123" // [!code ++] } // [!code ++] } ``` ```bash title="v1 cURL" icon="terminal" theme={null} curl -X POST https://api.unkey.dev/v1/ratelimits.setOverride \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "namespace": "api_requests", "identifier": "premium_user_456", "limit": 10000, "duration": 3600000 }' ``` ```bash title="v2 cURL" icon="terminal" theme={null} curl -X POST https://api.unkey.com/v2/ratelimits.setOverride \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "namespace": "api_requests", "identifier": "premium_user_456", "limit": 10000, "duration": 3600000 }' ``` *** ### GET /v1/ratelimits.getOverride → POST /v2/ratelimits.getOverride **Key Changes:** * HTTP method: GET → POST * Request format: Query parameters → Request body * Response format: Direct response → `{meta, data}` envelope ```json title="Get Override Request" icon="search" theme={null} { "namespace": "api_requests", "identifier": "premium_user_456" } ``` ```json title="Get Override Response Diff" icon="database" expandable theme={null} // v1 Response (direct response) { "id": "over_123", // [!code --] "identifier": "premium_user_456", // [!code --] "limit": 10000, // [!code --] "duration": 3600000, // [!code --] "async": false // [!code --] } // v2 Response { "meta": { // [!code ++] "requestId": "req_getoverride789" // [!code ++] }, // [!code ++] "data": { // [!code ++] "overrideId": "override_abc123", // [!code ++] "identifier": "premium_user_456", // [!code ++] "limit": 10000, // [!code ++] "duration": 3600000 // [!code ++] } // [!code ++] } ``` ```bash title="v1 cURL" icon="terminal" theme={null} curl -X GET "https://api.unkey.dev/v1/ratelimits.getOverride?identifier=premium_user_456&namespaceName=api_requests" \ -H "Authorization: Bearer " ``` ```bash title="v2 cURL" icon="terminal" theme={null} curl -X POST https://api.unkey.com/v2/ratelimits.getOverride \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "namespace": "api_requests", "identifier": "premium_user_456" }' ``` *** ### GET /v1/ratelimits.listOverrides → POST /v2/ratelimits.listOverrides **Purpose:** Get paginated list of all overrides in a namespace. **Key Changes:** * HTTP method: GET → POST * Request format: Query parameters → Request body * Response format: Direct response → `{meta, data}` envelope ```json title="List Overrides Request" icon="list" theme={null} { "namespace": "api_requests", "limit": 100, "cursor": "optional_cursor" } ``` ```json title="List Overrides Response Diff" icon="database" expandable theme={null} // v1 Response (direct response) { "overrides": [ // [!code --] { // [!code --] "id": "override_abc123", // [!code --] "identifier": "premium_user_456", // [!code --] "limit": 10000, // [!code --] "duration": 3600000, // [!code --] "async": false // [!code --] } // [!code --] ], // [!code --] "cursor": "next_page_cursor_here", // [!code --] "total": 42 // [!code --] } // v2 Response (envelope format) { "meta": { // [!code ++] "requestId": "req_listoverrides123" // [!code ++] }, // [!code ++] "data": { // [!code ++] "overrides": [ // [!code ++] { // [!code ++] "overrideId": "override_abc123", // [!code ++] "identifier": "premium_user_456", // [!code ++] "limit": 10000, // [!code ++] "duration": 3600000, // [!code ++] } // [!code ++] ], // [!code ++] "cursor": "next_page_cursor_here" // [!code ++] } // [!code ++] } ``` ```bash title="v1 cURL" icon="terminal" theme={null} curl -X GET "https://api.unkey.dev/v1/ratelimits.listOverrides?namespaceName=api_requests&limit=100" \ -H "Authorization: Bearer " ``` ```bash title="v2 cURL" icon="terminal" theme={null} curl -X POST https://api.unkey.com/v2/ratelimits.listOverrides \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "namespace": "api_requests", "limit": 100, "cursor": "optional_cursor" }' ``` *** ### POST /v1/ratelimits.deleteOverride → POST /v2/ratelimits.deleteOverride **Key Changes:** * Response format: Direct response → `{meta, data}` envelope ```json title="Delete Override Request" icon="trash" theme={null} { "namespace": "api_requests", "identifier": "premium_user_456" } ``` ```json title="Delete Override Response Diff" icon="check-circle" theme={null} // v1 Response (direct empty response) {} // [!code --] // v2 Response { "meta": { // [!code ++] "requestId": "req_deleteoverride999" // [!code ++] }, // [!code ++] "data": {} // [!code ++] } ``` ```bash title="v1 cURL" icon="terminal" theme={null} curl -X POST https://api.unkey.dev/v1/ratelimits.deleteOverride \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "namespace": "api_requests", "identifier": "premium_user_456" }' ``` ```bash title="v2 cURL" icon="terminal" theme={null} curl -X POST https://api.unkey.com/v2/ratelimits.deleteOverride \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "namespace": "api_requests", "identifier": "premium_user_456" }' ``` *** ## Key-Level Rate Limiting Changes ### v1 Single Rate Limit → v2 Multiple Named Rate Limits ```json title="Key Rate Limit Structure Migration" icon="key" expandable theme={null} // v1 Key Creation { "apiId": "api_123", "ratelimit": { // [!code --] "limit": 1000, // [!code --] "duration": 3600000, // [!code --] "async": true // [!code --] } // [!code --] } // v2 Key Creation { "apiId": "api_123", "ratelimits": [ // [!code ++] { // [!code ++] "name": "api_requests", // [!code ++] "limit": 1000, // [!code ++] "duration": 3600000, // [!code ++] "autoApply": true // [!code ++] }, // [!code ++] { // [!code ++] "name": "heavy_operations", // [!code ++] "limit": 10, // [!code ++] "duration": 60000, // [!code ++] "autoApply": false // [!code ++] } // [!code ++] ] // [!code ++] } ``` ```json title="Rate Limit Verification Migration" icon="shield-check" expandable theme={null} // v1 Key Verification Request { "key": "sk_123" } // v2 Key Verification Request with Named Rate Limits { "key": "sk_123", "ratelimits": [ { "name": "api_requests", "cost": 1 }, { "name": "heavy_operations", "cost": 5 } ] } ``` ```json title="Rate Limit Response Migration" icon="database" expandable theme={null} // v1 Response { "result": { "valid": true, "ratelimit": { // [!code --] "limit": 1000, // [!code --] "remaining": 999, // [!code --] "reset": 1672531200000 // [!code --] } // [!code --] } } // v2 Response { "data": { "valid": true, "ratelimits": [ // [!code ++] { // [!code ++] "id": "rl_123", // [!code ++] "name": "api_requests", // [!code ++] "limit": 1000, // [!code ++] "remaining": 999, // [!code ++] "reset": 1672531200000, // [!code ++] "exceeded": false, // [!code ++] "duration": 3600000, // [!code ++] "autoApply": true // [!code ++] }, // [!code ++] { // [!code ++] "id": "rl_456", // [!code ++] "name": "heavy_operations", // [!code ++] "limit": 10, // [!code ++] "remaining": 5, // [!code ++] "reset": 1672531200000, // [!code ++] "exceeded": false, // [!code ++] "duration": 60000, // [!code ++] "autoApply": false // [!code ++] } // [!code ++] ] // [!code ++] } } ``` *** ## Migration Patterns ### Response Format Migration ```typescript title="v1 vs v2: Response Handling" theme={null} // v1: Direct response access const rateLimit = await fetch('/v1/ratelimits.limit', { // [!code --] method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ namespace: 'api_calls', identifier: 'user_123', limit: 100, duration: 3600000 }) }); const data = await rateLimit.json(); // [!code --] const success = data.success; // v1 direct format // [!code --] const remaining = data.remaining; // [!code --] // v2: Access data through data field const rateLimit = await fetch('/v2/ratelimits.limit', { // [!code ++] method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ namespace: 'api_calls', identifier: 'user_123', limit: 100, duration: 3600000 }) }); const response = await rateLimit.json(); // [!code ++] const success = response.data.success; // v2 format // [!code ++] const remaining = response.data.remaining; // [!code ++] const requestId = response.meta.requestId; // for debugging // [!code ++] ``` ### Key-Level Rate Limiting Migration ```json title="v1 vs v2: Key Rate Limit Structure" theme={null} // v1: Single Rate Limit { "apiId": "api_123", "ratelimit": { // [!code --] "limit": 1000, // [!code --] "duration": 3600000, // [!code --] "async": true // [!code --] } // [!code --] } // v2: Multiple Named Rate Limits { "apiId": "api_123", "ratelimits": [ // [!code ++] { // [!code ++] "name": "general_requests", // [!code ++] "limit": 1000, // [!code ++] "duration": 3600000, // [!code ++] "autoApply": true // [!code ++] }, // [!code ++] { // [!code ++] "name": "expensive_ops", // [!code ++] "limit": 10, // [!code ++] "duration": 60000, // [!code ++] "autoApply": false // [!code ++] } // [!code ++] ] // [!code ++] } ``` ### Override Management Patterns ```typescript title="Override CRUD Operations" theme={null} // Set override (same in v1 & v2) const override = await fetch('/v2/ratelimits.setOverride', { method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ namespace: 'api_requests', identifier: 'premium_user', limit: 10000, duration: 3600000 }) }); // Get override (same in v1 & v2) const existing = await fetch('/v2/ratelimits.getOverride', { method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ namespace: 'api_requests', identifier: 'premium_user' }) }); const result = await existing.json(); const limit = result.data.limit; // v2: access via data ``` ```typescript title="v2: List and Batch Management" theme={null} // v2: List all overrides (new capability) const overrides = await fetch('/v2/ratelimits.listOverrides', { method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ namespace: 'api_requests', limit: 100 }) }); const result = await overrides.json(); const overrideList = result.data.overrides; // Process overrides in batches for (const override of overrideList) { if (override.limit < 1000) { // Update low-limit overrides await fetch('/v2/ratelimits.setOverride', { method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' }, body: JSON.stringify({ namespace: override.namespace, identifier: override.identifier, limit: 1000, duration: override.duration }) }); } } ``` *** ## Advanced Features in v2 ### Multiple Rate Limits per Key ```json title="Complex rate limiting setup" icon="bolt" expandable theme={null} { "apiId": "api_123", "ratelimits": [ { "name": "requests_per_minute", "limit": 60, "duration": 60000, "autoApply": true }, { "name": "requests_per_hour", "limit": 1000, "duration": 3600000, "autoApply": true }, { "name": "expensive_operations", "limit": 5, "duration": 300000, "autoApply": false } ] } ``` ### Named Rate Limit Targeting ```json title="Selective rate limit application" icon="target" theme={null} { "key": "sk_123", "ratelimits": [ { "name": "expensive_operations", "cost": 1 } ] } ``` Only applies cost to the "expensive\_operations" rate limit, leaving others unchanged. ### Batch Override Management ```typescript title="Managing multiple overrides" icon="list-check" theme={null} // List all overrides in namespace const overrides = await unkey.ratelimits.listOverrides({ namespace: "api_requests" }); // Process overrides in batches for (const override of overrides.data?.overrides || []) { if (override.limit < 1000) { // Update low-limit overrides await unkey.ratelimits.setOverride({ namespace: override.namespace, identifier: override.identifier, limit: 1000, duration: override.duration }); } } ``` *** ## Migration Checklist ### Response Format Updates * [ ] Change direct response access to `response.data` in all rate limiting calls * [ ] Extract and log `meta.requestId` from responses for debugging * [ ] Update error handling for new envelope response structure * [ ] Handle enhanced metadata in override responses ### Key-Level Rate Limiting Updates * [ ] Convert `ratelimit` object to `ratelimits` array in key creation * [ ] Add `name` field to all rate limit configurations * [ ] Remove `async` parameter * [ ] Update Key creation to use autoApply if necessary * [ ] Plan for multiple rate limits per key (different operation types) * [ ] Update key verification to handle multiple rate limits ### Override Management Updates * [ ] Update override response parsing from `result` to `data` * [ ] Utilize new `listOverrides` endpoint for enhanced management * [ ] Handle enhanced override metadata (overrideId, createdAt) * [ ] Implement cursor-based pagination for large override lists ### Enhanced Features * [ ] Implement named rate limit targeting in key verification * [ ] Use multiple rate limits for different operation types * [ ] Set up batch override management processes using listOverrides * [ ] Plan for granular rate limit control and monitoring * [ ] Use request IDs for debugging and support ### Advanced Rate Limiting Patterns * [ ] Implement selective rate limit application by name * [ ] Set up different costs for different rate limits * [ ] Use identity-level rate limiting combined with key-level limits * [ ] Build override management dashboards with enhanced data ### Testing * [ ] Test rate limiting with new response format * [ ] Verify override creation, retrieval, and deletion * [ ] Test multiple rate limits on single keys * [ ] Validate named rate limit targeting in key verification * [ ] Confirm override listing and pagination works correctly * [ ] Test batch override management workflows # Query key verification data Source: https://unkey.com/docs/api-reference/v2/analytics/query-key-verification-data https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/analytics.getVerifications Execute custom SQL queries against your key verification analytics. For complete documentation including available tables, columns, data types, query examples, see the schema reference in the API documentation. # Create API namespace Source: https://unkey.com/docs/api-reference/v2/apis/create-api-namespace https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apis.createApi Create an API namespace for organizing keys by environment, service, or product. Use this to separate production from development keys, isolate different services, or manage multiple products. Each API gets a unique identifier and dedicated infrastructure for secure key operations. **Important**: API names must be unique within your workspace and cannot be changed after creation. **Required Permissions** Your root key must have one of the following permissions: - `api.*.create_api` (to create APIs in any workspace) # Delete API namespace Source: https://unkey.com/docs/api-reference/v2/apis/delete-api-namespace https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apis.deleteApi Permanently delete an API namespace and immediately invalidate all associated keys. Use this for cleaning up development environments, retiring deprecated services, or removing unused resources. All keys in the namespace are immediately marked as deleted and will fail verification with `code=NOT_FOUND`. **Important**: This operation is immediate and permanent. Verify you have the correct API ID before deletion. If delete protection is enabled, disable it first through the dashboard or API configuration. **Required Permissions** Your root key must have one of the following permissions: - `api.*.delete_api` (to delete any API) - `api..delete_api` (to delete a specific API) # Get API namespace Source: https://unkey.com/docs/api-reference/v2/apis/get-api-namespace https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apis.getApi Retrieve basic information about an API namespace including its ID and name. Use this to verify an API exists before performing operations, get the human-readable name when you only have the API ID, or confirm access to a specific namespace. For detailed key information, use the `listKeys` endpoint instead. **Required Permissions** Your root key must have one of the following permissions: - `api.*.read_api` (to read any API) - `api..read_api` (to read a specific API) # List API keys Source: https://unkey.com/docs/api-reference/v2/apis/list-api-keys https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/apis.listKeys Retrieve a paginated list of API keys for dashboard and administrative interfaces. Use this to build key management dashboards, filter keys by user with `externalId`, or retrieve key details for administrative purposes. Each key includes status, metadata, permissions, and usage limits. **Important**: Set `decrypt: true` only in secure contexts to retrieve plaintext key values from recoverable keys. **Required Permissions** Your root key must have one of the following permissions for basic key listing: - `api.*.read_key` (to read keys from any API) - `api..read_key` (to read keys from a specific API) Additionally, you need read access to the API itself: - `api.*.read_api` or `api..read_api` Additional permission required for decrypt functionality: - `api.*.decrypt_key` or `api..decrypt_key` # Authentication Source: https://unkey.com/docs/api-reference/v2/auth Securely authenticating with the Unkey API Almost all Unkey API endpoints require authentication using a root key. Root keys provide access to your Unkey resources based on their assigned permissions. ## Bearer Authentication Authentication is performed using HTTP Bearer authentication in the `Authorization` header: ```bash theme={null} Authorization: Bearer unkey_1234567890 ``` Example request: ```bash theme={null} curl -X POST "https://api.unkey.com/v2/keys.createKey" \ -H "Authorization: Bearer unkey_1234567890" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_1234" }' ``` ## Security Best Practices Never expose your root key in client-side code or include it in public repositories. For frontend applications, always use a backend server to proxy requests to the Unkey API. ## Root Key Management Root keys can be created and managed through the Unkey dashboard. We recommend: 1. **Using Different Keys for Different Environments**: Maintain separate root keys for development, staging, and production 2. **Rotating Keys Regularly**: Create new keys periodically and phase out old ones 3. **Setting Clear Key Names**: Name your keys according to their use case for better manageability ## Key Permissions System Unkey implements a sophisticated RBAC (Role-Based Access Control) system for root keys. Permissions are defined as tuples of: * **ResourceType**: The category of resource (api, ratelimit, rbac, identity) * **ResourceID**: The specific resource instance * **Action**: The operation to perform on that resource ### Available Resource Types | Resource Type | Description | | ------------- | ------------------------------------------------- | | `api` | API-related resources, such as endpoints and keys | | `ratelimit` | Rate limiting resources and configuration | | `rbac` | Permissions and roles management | | `identity` | User and identity management | ### Permission Examples Specific permission to manage a single API: ``` api.api_1234.read_api api.api_1234.update_api ``` Wildcard permission to manage all rate limit namespaces: ``` ratelimit.*.create_namespace ratelimit.*.read_namespace ``` When creating root keys, you can specify exactly what actions they're allowed to perform. ## Authentication Errors If your authentication fails, you'll receive a 401 Unauthorized or 403 Forbidden response with an error message: ```json theme={null} { "meta": { "requestId": "req_abc123xyz789" }, "error": { "title": "Unauthorized", "detail": "The provided root key is invalid or has been revoked", "status": 401, "type": "https://unkey.com/docs/errors/unauthorized" } } ``` If your key is valid but lacks sufficient permissions, you'll receive a 403 Forbidden response: ```json theme={null} { "meta": { "requestId": "req_abc123xyz789" }, "error": { "title": "Forbidden", "detail": "Your key does not have the required 'api.api_1234.update_api' permission", "status": 403, "type": "https://unkey.com/docs/errors/forbidden" } } ``` Common authentication issues include: * Missing the Authorization header * Invalid key format * Revoked or expired root key * Using a key with insufficient permissions # Create deployment Source: https://unkey.com/docs/api-reference/v2/deploy/create-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deploy.createDeployment **INTERNAL** - This endpoint is internal and may change without notice. Not recommended for production use. Creates a new deployment for a project using either a pre-built Docker image or build context. **Authentication**: Requires a valid root key with appropriate permissions. # Get deployment Source: https://unkey.com/docs/api-reference/v2/deploy/get-deployment https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/deploy.getDeployment **INTERNAL** - This endpoint is internal and may change without notice. Not recommended for production use. Retrieves deployment information including status, error messages, and steps. **Authentication**: Requires a valid root key with appropriate permissions. # Error Handling Source: https://unkey.com/docs/api-reference/v2/errors Understanding and working with API errors Error responses maintain the same top-level structure as successful responses, but with an `error` object instead of `data`: ```json theme={null} { "meta": { "requestId": "req_abc123xyz789" }, "error": { "title": "Validation Error", "detail": "You must provide a valid API ID.", "status": 400, "type": "https://unkey.com/docs/errors/validation-error", "errors": [ { "location": "body.apiId", "message": "API not found", "fix": "Provide a valid API ID or create a new API" } ] } } ``` ## Error Format Our error format follows RFC7807 Problem Details standard within our consistent envelope structure, providing: * **title**: A short, human-readable summary of the problem * **detail**: A human-readable explanation specific to this occurrence * **status**: The HTTP status code (also returned in the HTTP response) * **type**: A URI reference that identifies the problem type and points to documentation * **errors**: (Optional) An array of specific validation errors when multiple issues occur ## Common Error Types | Status | Error Type | Description | | ------ | --------------------- | ------------------------------------------------ | | 400 | validation-error | The request body failed validation | | 401 | unauthorized | Missing or invalid authorization | | 403 | forbidden | Valid authorization but insufficient permissions | | 404 | not-found | The requested resource was not found | | 409 | conflict | The request conflicts with the current state | | 429 | rate-limited | You've exceeded your rate limit | | 500 | internal-server-error | An unexpected error occurred on our servers | ## Validation Errors For validation errors, we provide detailed information about each failed validation: * **location**: Where in the request the error occurred (e.g., `body.name`, `query.limit`) * **message**: What went wrong with the specific field * **fix**: (When possible) A suggestion for how to fix the issue ## Error Recovery Our error messages are designed to be actionable. Each error includes: 1. A clear explanation of what went wrong 2. Often, a suggestion for how to fix the issue 3. For validation errors, the specific fields that failed validation ## Using the Request ID for Support When reporting issues to our support team, always include the `requestId` from the error response. This unique identifier allows us to quickly locate the specific request in our logs and provide faster, more accurate assistance. ## Error Handling Best Practices 1. **Check for Status Codes**: Always check HTTP status codes first to determine broad error categories 2. **Extract Error Details**: Parse the error object for detailed information 3. **Implement Retries Carefully**: Only retry on 5xx errors or when explicitly advised 4. **Log Complete Errors**: Log the full error response for debugging purposes Example error handling in JavaScript: ```javascript theme={null} try { const response = await fetch('https://api.unkey.com/v2/keys.create', { method: 'POST', headers: { 'Authorization': `Bearer ${rootKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(keyData) }); const data = await response.json(); if (!response.ok) { // Extract and handle the error const { meta, error } = data; console.error(`Error ${error.status}: ${error.title}`, { requestId: meta.requestId, detail: error.detail, docs: error.type }); // Handle validation errors specifically if (error.errors) { error.errors.forEach(err => { console.error(`- ${err.location}: ${err.message}`); }); } throw new Error(`API Error: ${error.detail}`); } return data.data; // Return just the data portion on success } catch (err) { // Handle network errors or other exceptions console.error('Request failed:', err); throw err; } ``` # Create Identity Source: https://unkey.com/docs/api-reference/v2/identities/create-identity https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.createIdentity Create an identity to group multiple API keys under a single entity. Identities enable shared rate limits and metadata across all associated keys. Perfect for users with multiple devices, organizations with multiple API keys, or when you need unified rate limiting across different services. **Important** Requires `identity.*.create_identity` permission # Delete Identity Source: https://unkey.com/docs/api-reference/v2/identities/delete-identity https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.deleteIdentity Permanently delete an identity. This operation cannot be undone. Use this for data cleanup, compliance requirements, or when removing entities from your system. > **Important** > Requires `identity.*.delete_identity` permission > Associated API keys remain functional but lose shared resources > External ID becomes available for reuse immediately # Get Identity Source: https://unkey.com/docs/api-reference/v2/identities/get-identity https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.getIdentity Retrieve an identity by external ID. Returns metadata, rate limits, and other associated data. Use this to check if an identity exists, view configurations, or build management dashboards. > **Important** > Requires `identity.*.read_identity` permission # List Identities Source: https://unkey.com/docs/api-reference/v2/identities/list-identities https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.listIdentities Get a paginated list of all identities in your workspace. Returns metadata and rate limit configurations. Perfect for building management dashboards, auditing configurations, or browsing your identities. > **Important** > Requires `identity.*.read_identity` permission # Update Identity Source: https://unkey.com/docs/api-reference/v2/identities/update-identity https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/identities.updateIdentity Update an identity's metadata and rate limits. Only specified fields are modified - others remain unchanged. Perfect for subscription changes, plan upgrades, or updating user information. Changes take effect immediately. > **Important** > Requires `identity.*.update_identity` permission > Rate limit changes propagate within 30 seconds # Add key permissions Source: https://unkey.com/docs/api-reference/v2/keys/add-key-permissions https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.addPermissions Add permissions to a key without affecting existing permissions. Use this for privilege upgrades, enabling new features, or plan changes that grant additional capabilities. Permissions granted through roles remain unchanged. **Important**: Changes take effect immediately with up to 30-second edge propagation. **Required Permissions** Your root key must have one of the following permissions: - `api.*.update_key` (to update keys in any API) - `api..update_key` (to update keys in a specific API) **Side Effects** Invalidates the key cache for immediate effect, and makes permissions available for verification within 30 seconds across all regions. # Add key roles Source: https://unkey.com/docs/api-reference/v2/keys/add-key-roles https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.addRoles Add roles to a key without affecting existing roles or permissions. Use this for privilege upgrades, enabling new feature sets, or subscription changes that grant additional role-based capabilities. Direct permissions remain unchanged. **Important**: Changes take effect immediately with up to 30-second edge propagation. **Required Permissions** Your root key must have one of the following permissions: - `api.*.update_key` (to update keys in any API) - `api..update_key` (to update keys in a specific API) **Side Effects** Invalidates the key cache for immediate effect, and makes role assignments available for verification within 30 seconds across all regions. # Create API key Source: https://unkey.com/docs/api-reference/v2/keys/create-api-key https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.createKey Create a new API key for user authentication and authorization. Use this endpoint when users sign up, upgrade subscription tiers, or need additional keys. Keys are cryptographically secure and unique to the specified API namespace. **Important**: The key is returned only once. Store it immediately and provide it to your user, as it cannot be retrieved later. **Common use cases:** - Generate keys for new user registrations - Create additional keys for different applications - Issue keys with specific permissions or limits **Required Permissions** Your root key needs one of: - `api.*.create_key` (create keys in any API) - `api..create_key` (create keys in specific API) # Delete API keys Source: https://unkey.com/docs/api-reference/v2/keys/delete-api-keys https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.deleteKey Delete API keys permanently from user accounts or for cleanup purposes. Use this for user-requested key deletion, account deletion workflows, or cleaning up unused keys. Keys are immediately invalidated. Two modes: soft delete (default, preserves audit records) and permanent delete. **Important**: For temporary access control, use `updateKey` with `enabled: false` instead of deletion. **Required Permissions** Your root key must have one of the following permissions: - `api.*.delete_key` (to delete keys in any API) - `api..delete_key` (to delete keys in a specific API) # Get API key Source: https://unkey.com/docs/api-reference/v2/keys/get-api-key https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.getKey Retrieve detailed key information for dashboard interfaces and administrative purposes. Use this to build key management dashboards showing users their key details, status, permissions, and usage data. You can identify keys by `keyId` or the actual key string. **Important**: Set `decrypt: true` only in secure contexts to retrieve plaintext key values from recoverable keys. **Required Permissions** Your root key must have one of the following permissions for basic key information: - `api.*.read_key` (to read keys from any API) - `api..read_key` (to read keys from a specific API) Additional permission required for decrypt functionality: - `api.*.decrypt_key` or `api..decrypt_key` # Get API key by hash Source: https://unkey.com/docs/api-reference/v2/keys/get-api-key-by-hash https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.whoami Find out what key this is. **Required Permissions** Your root key must have one of the following permissions for basic key information: - `api.*.read_key` (to read keys from any API) - `api..read_key` (to read keys from a specific API) If your rootkey lacks permissions but the key exists, we may return a 404 status here to prevent leaking the existance of a key to unauthorized clients. If you believe that a key should exist, but receive a 404, please double check your root key has the correct permissions. # Migrate API key(s) Source: https://unkey.com/docs/api-reference/v2/keys/migrate-api-keys https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.migrateKeys Returns HTTP 200 even on partial success; hashes that could not be migrated are listed under `data.failed`. **Required Permissions** Your root key must have one of the following permissions for basic key information: - `api.*.create_key` (to migrate keys to any API) - `api..create_key` (to migrate keys to a specific API) # Remove key permissions Source: https://unkey.com/docs/api-reference/v2/keys/remove-key-permissions https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.removePermissions Remove permissions from a key without affecting existing roles or other permissions. Use this for privilege downgrades, removing temporary access, or plan changes that revoke specific capabilities. Permissions granted through roles remain unchanged. **Important**: Changes take effect immediately with up to 30-second edge propagation. **Required Permissions** Your root key must have one of the following permissions: - `api.*.update_key` (to update keys in any API) - `api..update_key` (to update keys in a specific API) **Side Effects** Invalidates the key cache for immediate effect, and makes permission changes available for verification within 30 seconds across all regions. # Remove key roles Source: https://unkey.com/docs/api-reference/v2/keys/remove-key-roles https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.removeRoles Remove roles from a key without affecting direct permissions or other roles. Use this for privilege downgrades, removing temporary access, or subscription changes that revoke specific role-based capabilities. Direct permissions remain unchanged. **Important**: Changes take effect immediately with up to 30-second edge propagation. **Required Permissions** Your root key must have one of the following permissions: - `api.*.update_key` (to update keys in any API) - `api..update_key` (to update keys in a specific API) **Side Effects** Invalidates the key cache for immediate effect, and makes role changes available for verification within 30 seconds across all regions. # Reroll Key Source: https://unkey.com/docs/api-reference/v2/keys/reroll-key https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.rerollKey Generate a new API key while preserving the configuration from an existing key. This operation creates a fresh key with a new token while maintaining all settings from the original key: - Permissions and roles - Custom metadata - Rate limit configurations - Identity associations - Remaining credits - Recovery settings **Key Generation:** - The system attempts to extract the prefix from the original key - If prefix extraction fails, the default API prefix is used - Key length follows the API's default byte configuration (or 16 bytes if not specified) **Original Key Handling:** - The original key will be revoked after the duration specified in `expiration` - Set `expiration` to 0 to revoke immediately - This allows for graceful key rotation with an overlap period Common use cases include: - Rotating keys for security compliance - Issuing replacement keys for compromised credentials - Creating backup keys with identical permissions **Important:** Analytics and usage metrics are tracked at both the key level AND identity level. If the original key has an identity, the new key will inherit it, allowing you to track usage across both individual keys and the overall identity. **Required Permissions** Your root key must have: - `api.*.create_key` or `api..create_key` - `api.*.encrypt_key` or `api..encrypt_key` (only when the original key is recoverable) # Set key permissions Source: https://unkey.com/docs/api-reference/v2/keys/set-key-permissions https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.setPermissions Replace all permissions on a key with the specified set in a single atomic operation. Use this to synchronize with external systems, reset permissions to a known state, or apply standardized permission templates. Permissions granted through roles remain unchanged. **Important**: Changes take effect immediately with up to 30-second edge propagation. **Required Permissions** Your root key must have one of the following permissions: - `api.*.update_key` (to update keys in any API) - `api..update_key` (to update keys in a specific API) **Side Effects** Invalidates the key cache for immediate effect, and makes permission changes available for verification within 30 seconds across all regions. # Set key roles Source: https://unkey.com/docs/api-reference/v2/keys/set-key-roles https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.setRoles Replace all roles on a key with the specified set in a single atomic operation. Use this to synchronize with external systems, reset roles to a known state, or apply standardized role templates. Direct permissions are never affected. **Important**: Changes take effect immediately with up to 30-second edge propagation. **Required Permissions** Your root key must have one of the following permissions: - `api.*.update_key` (to update keys in any API) - `api..update_key` (to update keys in a specific API) **Side Effects** Invalidates the key cache for immediate effect, and makes role changes available for verification within 30 seconds across all regions. # Update key credits Source: https://unkey.com/docs/api-reference/v2/keys/update-key-credits https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.updateCredits Update credit quotas in response to plan changes, billing cycles, or usage purchases. Use this for user upgrades/downgrades, monthly quota resets, credit purchases, or promotional bonuses. Supports three operations: set, increment, or decrement credits. Set to null for unlimited usage. **Important**: Setting unlimited credits automatically clears existing refill configurations. **Required Permissions** Your root key must have one of the following permissions: - `api.*.update_key` (to update keys in any API) - `api..update_key` (to update keys in a specific API) **Side Effects** Credit updates remove the key from cache immediately. Setting credits to unlimited automatically clears any existing refill settings. Changes take effect instantly but may take up to 30 seconds to propagate to all edge regions. # Update key settings Source: https://unkey.com/docs/api-reference/v2/keys/update-key-settings https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.updateKey Update key properties in response to plan changes, subscription updates, or account status changes. Use this for user upgrades/downgrades, role modifications, or administrative changes. Supports partial updates - only specify fields you want to change. Set fields to null to clear them. **Important**: Permissions and roles are replaced entirely. Use dedicated add/remove endpoints for incremental changes. **Required Permissions** Your root key must have one of the following permissions: - `api.*.update_key` (to update keys in any API) - `api..update_key` (to update keys in a specific API) **Side Effects** If you specify an `externalId` that doesn't exist, a new identity will be automatically created and linked to the key. Permission updates will auto-create any permissions that don't exist in your workspace. Changes take effect immediately but may take up to 30 seconds to propagate to all edge regions due to cache invalidation. # Verify API key Source: https://unkey.com/docs/api-reference/v2/keys/verify-api-key https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/keys.verifyKey Verify an API key's validity and permissions for request authentication. Use this endpoint on every incoming request to your protected resources. It checks key validity, permissions, rate limits, and usage quotas in a single call. **Important**: Returns HTTP 200 for all verification outcomes — check the `valid` field in response data to determine if the key is authorized. A 429 may be returned if the workspace exceeds its API rate limit. **Common use cases:** - Authenticate API requests before processing - Enforce permission-based access control - Track usage and apply rate limits **Required Permissions** Your root key needs one of: - `api.*.verify_key` (verify keys in any API) - `api..verify_key` (verify keys in specific API) **Note**: If your root key has no verify permissions at all, you will receive a `403 Forbidden` error. If your root key has verify permissions for a different API than the key you're verifying, you will receive a `200` response with `code: NOT_FOUND` to avoid leaking key existence. # Overview Source: https://unkey.com/docs/api-reference/v2/overview Our philosophy for building developer-friendly APIs ## Core Principles * **Clear Communication**: Structured responses make success and failure equally informative * **Practical Over Purist**: We make pragmatic choices rather than rigidly adhering to any single paradigm * **Predictable Patterns**: Once you learn one endpoint, you'll understand them all ## Response Structure All API responses follow a consistent structure, making them predictable and easy to parse: ```json theme={null} { "meta": { "requestId": "req_abc123xyz789" }, "data": { // Operation-specific response data } } ``` For paginated responses, we include a pagination object: ```json theme={null} { "meta": { "requestId": "req_abc123xyz789", "timestamp": "2023-11-08T15:22:30Z" }, "data": [ // Array of items ], "pagination": { "cursor": "cursor_xyz123", "hasMore": true } } ``` ## Working with the API ### Always Use the Request ID Every response includes a unique `requestId` in the metadata. When seeking support or debugging issues, always include this ID as it allows us to trace exactly what happened with your request. You can also search for the `requestId` yourself in the [logs](https://app.unkey.com/logs). ### Handling Pagination For endpoints that return lists of items: 1. Make your initial request 2. Check for `pagination.hasMore` to see if more items exist 3. If `true`, use the `pagination.cursor` value in your next request Example: ```js theme={null} // First request const response = await fetch('https://api.unkey.com/v2/keys.listKeys', { method: 'POST', headers: { Authorization: `Bearer ${rootKey}` }, body: JSON.stringify({ apiId: 'api_123' }) }); // Follow-up request with cursor if (response.pagination?.hasMore) { const nextPage = await fetch('https://api.unkey.com/v2/keys.listKeys', { method: 'POST', headers: { Authorization: `Bearer ${rootKey}` }, body: JSON.stringify({ apiId: 'api_123', cursor: response.pagination.cursor }) }); } ``` ## Versioning Our API uses a major version in the URL (e.g., `/v2/`) and maintains backwards compatibility within each major version. When we need to make breaking changes, we increment the major version number. We communicate deprecations well in advance, giving you time to update your integration before endpoints are removed. # Create permission Source: https://unkey.com/docs/api-reference/v2/permissions/create-permission https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.createPermission Create a new permission to define specific actions or capabilities in your RBAC system. Permissions can be assigned directly to API keys or included in roles. Use hierarchical naming patterns like `documents.read`, `admin.users.delete`, or `billing.invoices.create` for clear organization. **Important:** Permission names must be unique within the workspace. Once created, permissions are immediately available for assignment. **Required Permissions** Your root key must have the following permission: - `rbac.*.create_permission` # Create role Source: https://unkey.com/docs/api-reference/v2/permissions/create-role https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.createRole Create a new role to group related permissions for easier management. Roles enable consistent permission assignment across multiple API keys. **Important:** Role names must be unique within the workspace. Once created, roles are immediately available for assignment. **Required Permissions** Your root key must have the following permission: - `rbac.*.create_role` # Delete permission Source: https://unkey.com/docs/api-reference/v2/permissions/delete-permission https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.deletePermission Remove a permission from your workspace. This also removes the permission from all API keys and roles. **Important:** This operation cannot be undone and immediately affects all API keys and roles that had this permission assigned. **Required Permissions** Your root key must have the following permission: - `rbac.*.delete_permission` # Delete role Source: https://unkey.com/docs/api-reference/v2/permissions/delete-role https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.deleteRole Remove a role from your workspace. This also removes the role from all assigned API keys. **Important:** This operation cannot be undone and immediately affects all API keys that had this role assigned. **Required Permissions** Your root key must have the following permission: - `rbac.*.delete_role` # Get permission Source: https://unkey.com/docs/api-reference/v2/permissions/get-permission https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.getPermission Retrieve details about a specific permission including its name, description, and metadata. **Required Permissions** Your root key must have the following permission: - `rbac.*.read_permission` # Get role Source: https://unkey.com/docs/api-reference/v2/permissions/get-role https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.getRole Retrieve details about a specific role including its assigned permissions. **Required Permissions** Your root key must have the following permission: - `rbac.*.read_role` # List permissions Source: https://unkey.com/docs/api-reference/v2/permissions/list-permissions https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.listPermissions Retrieve all permissions in your workspace. Results are paginated and sorted by their id. **Required Permissions** Your root key must have the following permission: - `rbac.*.read_permission` # List roles Source: https://unkey.com/docs/api-reference/v2/permissions/list-roles https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/permissions.listRoles Retrieve all roles in your workspace including their assigned permissions. Results are paginated and sorted by their id. **Required Permissions** Your root key must have the following permission: - `rbac.*.read_role` # Apply multiple rate limit checks Source: https://unkey.com/docs/api-reference/v2/ratelimit/apply-multiple-rate-limit-checks https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.multiLimit Check and enforce multiple rate limits in a single request for any identifiers (user IDs, IP addresses, API clients, etc.). Use this to efficiently check multiple rate limits at once. Each rate limit check is independent and returns its own result with a top-level `passed` indicator showing if all checks succeeded. **Response Codes**: Rate limit checks return HTTP 200 regardless of whether limits are exceeded — check the `passed` field to see if all limits passed, or the `success` field in each individual result. A 429 may be returned if the workspace exceeds its API rate limit. Other 4xx responses indicate auth, namespace existence/deletion, or validation errors (e.g., 410 Gone for deleted namespaces). 5xx responses indicate server errors. **Required Permissions** Your root key must have one of the following permissions: - `ratelimit.*.limit` (to check limits in any namespace) - `ratelimit..limit` (to check limits in all specific namespaces being checked) # Apply rate limiting Source: https://unkey.com/docs/api-reference/v2/ratelimit/apply-rate-limiting https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.limit Check and enforce rate limits for any identifier (user ID, IP address, API client, etc.). Use this for rate limiting beyond API keys - limit users by ID, IPs by address, or any custom identifier. Supports namespace organization, variable costs, and custom overrides. **Response Codes**: Rate limit checks return HTTP 200 regardless of whether the limit is exceeded — check the `success` field in the response to determine if the request should be allowed. A 429 may be returned if the workspace exceeds its API rate limit. Other 4xx responses indicate auth, namespace existence/deletion, or validation errors (e.g., 410 Gone for deleted namespaces). 5xx responses indicate server errors. **Required Permissions** Your root key must have one of the following permissions: - `ratelimit.*.limit` (to check limits in any namespace) - `ratelimit..limit` (to check limits in a specific namespace) # Delete ratelimit override Source: https://unkey.com/docs/api-reference/v2/ratelimit/delete-ratelimit-override https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.deleteOverride Permanently remove a rate limit override. Affected identifiers immediately revert to the namespace default. Use this to remove temporary overrides, reset identifiers to standard limits, or clean up outdated rules. **Important:** Deletion is immediate and permanent. The override cannot be recovered and must be recreated if needed again. **Permissions:** Requires `ratelimit.*.delete_override` or `ratelimit..delete_override` # Get ratelimit override Source: https://unkey.com/docs/api-reference/v2/ratelimit/get-ratelimit-override https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.getOverride Retrieve the configuration of a specific rate limit override by its identifier. Use this to inspect override configurations, audit rate limiting policies, or debug rate limiting behavior. **Important:** The identifier must match exactly as specified when creating the override, including wildcard patterns. **Permissions:** Requires `ratelimit.*.read_override` or `ratelimit..read_override` # List ratelimit overrides Source: https://unkey.com/docs/api-reference/v2/ratelimit/list-ratelimit-overrides https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.listOverrides Retrieve a paginated list of all rate limit overrides in a namespace. Use this to audit rate limiting policies, build admin dashboards, or manage override configurations. **Important:** Results are paginated. Use the cursor parameter to retrieve additional pages when more results are available. **Permissions:** Requires `ratelimit.*.read_override` or `ratelimit..read_override` # Set ratelimit override Source: https://unkey.com/docs/api-reference/v2/ratelimit/set-ratelimit-override https://spec.speakeasy.com/unkey/unkey/openapi-json-with-code-samples post /v2/ratelimit.setOverride Create or update a custom rate limit for specific identifiers, bypassing the namespace default. Use this to create premium tiers with higher limits, apply stricter limits to specific users, or implement emergency throttling. **Important:** Overrides take effect immediately and completely replace the default limit for matching identifiers. Use wildcard patterns (e.g., `premium_*`) to match multiple identifiers. **Permissions:** Requires `ratelimit.*.set_override` or `ratelimit..set_override` # RPC-Style API Source: https://unkey.com/docs/api-reference/v2/rpc Understanding our action-oriented API design We use an RPC (Remote Procedure Call) style API that focuses on *actions* rather than resources. This means endpoints represent specific operations: ``` https://api.unkey.com/v2/{service}.{procedure} ``` For example: * `POST /v2/keys.createKey` - Create a new API key * `POST /v2/ratelimit.limit` - Check or enforce a rate limit We chose this approach because it maps directly to the operations developers want to perform, making the API intuitive to use. ## HTTP Methods We exclusively use POST for all operations. While this deviates from REST conventions, it provides several advantages: 1. **Consistent Request Pattern**: All requests follow the same pattern regardless of operation 2. **Rich Query Parameters**: Complex filtering and querying without URL length limitations 3. **Security and Compatibility**: Avoids issues with proxies or firewalls logging potentially sensitive parameters in the url ## Request Format All requests should: * Use the POST HTTP method * Include a Content-Type header set to application/json * Include an Authorization header (see Authentication documentation) * Send parameters as a JSON object in the request body Example: ```bash theme={null} curl -X POST "https://api.unkey.com/v2/keys.createKey" \ -H "Authorization: Bearer root_1234567890" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_1234", "name": "Production API Key" }' ``` ## Service Namespaces Our API is organized into logical service namespaces that group related procedures: * **keys** - API key management (create, verify, revoke) * **apis** - API configuration and settings * **ratelimit** - Rate limiting services * **analytics** - Usage and performance data * **identities** - Identity management * **permissions** - Permission management Each namespace contains multiple procedures that perform specific actions within that domain. ## Benefits of RPC Design We believe our RPC-style approach offers significant benefits: 1. **Clarity of Intent**: Endpoint names clearly communicate the action being performed 2. **Natural Code Mapping**: Endpoints naturally map to code and user intent (`keys.createKey()` instead of `POST /keys`) 3. **Complex Operations**: Supports complex operations that don't map well to REST's resource model 4. **Flexibility**: Allows for more flexible request structures without being constrained by URL parameters # Example Source: https://unkey.com/docs/apis/features/authorization/example RBAC in the almost-real world Let's look at an example app for allowing your users to manage domains. As part of the API, your users will be able to perform CRUD operations against domains or individual dns records. Users of our app can have the following permissions: * `domain.delete_domain` * `domain.dns.create_record` * `domain.dns.read_record` * `domain.dns.update_record` * `domain.dns.delete_record` * `domain.create_domain` * `domain.read_domain` * `domain.update_domain` Sign into your [dashboard](https://app.unkey.com). * For `Roles`, navigate to the `Authorization/Roles`. Default when navigating to `Authorization`. * For `Permissions`, navigate to the `Authorization/Permissions`. Create them in your `Authorization/Permissions` page. Use the button in the upper right. `+ Create new permission` Example permissions We define the following roles: * `admin`: An admin can do everything. * `dns.manager`: Can create, read, update and delete dns records but not access the domain itself. * `read-only`: Can read domain or dns record information. Create them in your `Authorization/Roles` page. Use the button in the upper right. `+ Create new role` Example roles For each role, we need to connect the permissions it should have. Admin roles dns.manager roles read-only roles Now that we have permissions and roles in place, we can connect them to keys. 1. In the sidebar, click on one of your APIs 2. Next click on keys in the expanded API you selected. Breadcrumb Navigation 3. On the key you want to use, click on the action menu (`...`) at the end of that table row. 4. Select `Manage roles and permissions..` Unconnected roles and permissions 5. You can connect a role to your key by using the `Assign role` input. Let's give this key the `dns.manager` and `read-only` roles. Unconnected roles and permissions As you can see, the key now contains 2 `roles` and 5 `permissions` shown just above the Roles section: You can attach roles to a key when creating it by providing the role names as an array: ```bash theme={null} curl -XPOST \ --url https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer ${ROOT_KEY}" \ -H "Content-Type: application/json" \ -d '{ "apiId": "${API_ID}", "roles": [ "role1", "role2", "role3" ] }' ``` See [here](/api-reference/v2/keys/create-api-key) for details. Now you can verify this key and perform permission checks. [Read more](/apis/features/authorization/verifying) # Authorization Overview Source: https://unkey.com/docs/apis/features/authorization/introduction Control what each API key can access with roles and permissions Authorization controls *what* an authenticated key can do. While verification answers "is this key valid?", authorization answers "can this key perform this action?" Unkey provides Role-Based Access Control (RBAC) that lets you: * Define **permissions** (like `documents.read`, `billing.write`) * Group permissions into **roles** (like `admin`, `editor`, `viewer`) * Attach roles or permissions directly to keys * Check permissions during verification ## When to use this Different customers get different feature access. Enterprise keys can do more than free-tier keys. Admin keys can delete resources, editor keys can modify, viewer keys can only read. Only keys with `beta.access` permission can use new features. Keys can only access specific resources: `project.123.read`, `project.456.write`. ## How it works Create permissions that map to actions in your app: `documents.read`, `documents.write`, `users.delete`. Group permissions into roles for easier management. An `editor` role might include `documents.read` and `documents.write`. When creating or updating keys, assign roles or direct permissions. Pass a permission query when verifying. Unkey checks if the key has the required permissions. ## Quick example ```typescript theme={null} // When verifying, check for required permission try { const { meta, data } = await unkey.keys.verifyKey({ key: "sk_...", permissions: "documents.write", // Key must have this permission }); if (!data.valid) { // Either invalid key OR missing permission console.log(data.code); // "VALID", "FORBIDDEN", "INSUFFICIENT_PERMISSIONS", etc. } } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Permissions vs Roles | Concept | What it is | Example | | -------------- | ---------------------- | ----------------------------------------------------------- | | **Permission** | A specific action | `documents.read`, `billing.manage` | | **Role** | A group of permissions | `admin` = all permissions, `viewer` = read-only permissions | You can attach **either** (or both) directly to keys: * Attach roles when you want predefined access levels * Attach permissions directly for fine-grained control ## Next steps Set up your authorization structure in the dashboard Check permissions when verifying keys See a complete implementation # Roles and Permissions Source: https://unkey.com/docs/apis/features/authorization/roles-and-permissions In RBAC, roles represent a collection of permissions. Each role defines a set of actions or operations that a user with that role can perform. Permissions can be associated with various resources within your application, such as endpoints, data objects, or functionality. Common roles may include: * `Administrator`: Has full access to all resources and functionality. * `Editor`: Can create, read, update, and delete specific resources. * `Viewer`: Can only view resources but cannot modify them. ## Roles Creating, updating and deleting roles is available in the dashboard. ### Create 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click `Create New Role`. 4. Enter a unique name for your role. 5. Enter a description for your role. (Optional) 6. Assign keys and permissions to the role. (Optional) 7. Click `Create new role`. After the role is created, you are forwarded and can update/delete the role or connect existing permissions. ### Update 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on the role you want to update. Optionally you can also click the action menu (`...`) to the right of the role. 4. Make changes to the role as needed. 5. Click `Update role`. ### Delete 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on the action menu (`...`) to the right of the role you want to delete. 4. Click `Delete role` in the popup menu. 5. Toggle the checkbox confirming the deletion. 6. Click `Delete role` button. ## Permissions Creating, updating and deleting permissions is available in the dashboard. ### Create 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on `Permissions` in the left sidebar dropdown under the `Authorization` section. 4. Click `Create New Permission`. 5. Enter a human readable name for your permission. 6. Enter a unique identifier slug. 7. Enter a description for your permission. (Optional) 8. Click `Create new permission`. ### Update 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on `Permissions` in the left sidebar dropdown under the `Authorization` section. 4. Click on the permission you want to update. Optionally you can also click the action menu (`...`) to the right of the permission. 5. Make changes to the name, slug, and description as needed. 6. Click `Update permission`. ### Delete 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on `Permissions` in the left sidebar dropdown under the `Authorization` section. 4. Click on the action menu (`...`) to the right of the permission you want to delete. 5. Click `Delete permission`. 6. Toggle the checkbox confirming the deletion. 7. Click `Delete permission` button. ## Connecting roles and permissions After you have created at least 1 role and 1 permission, you can start associating them with each other. 1. From the unkey dashboard [app.unkey.com](https://app.unkey.com). 2. Navigate to the `Authorization` section in the left sidebar. 3. Click on the role you want to update. Optionally you can also click the action menu (`...`) to the right of the role. 4. Select the `Add permissions` input. Each item you select will be added to the selected permissions list below the input. This can be repeated to connect multiple permissions. 5. Click `Update role`. Role Update Dialog ## Connecting roles to keys 1. In the sidebar, click on one of your APIs. 2. In the sub menu, click on Keys. Sidebar Navigation 3. Select one of your existing keys by clicking the action menu (`...`) to the right of the key. 4. Select the `Manage roles and Permissions...` option. 5. Select a `role` or `permission` from appropriate input. 6. Click `Update key`. Key Authorization Update Dialog ## Creating keys When a user of your app creates a new key, you can attach zero, one or multiple previously created roles to the key. ```bash theme={null} curl -XPOST \ --url https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer ${ROOT_KEY}" \ -H "Content-Type: application/json" \ -d '{ "apiId": "${API_ID}", "roles": [ "role1", "role2", "role3" ] }' ``` See [here](/api-reference/v2/keys/create-api-key) for details. # Verifying Permissions Source: https://unkey.com/docs/apis/features/authorization/verifying Check if a key has the required permissions during verification When verifying a key, you can check if it has specific permissions. If the key lacks the required permissions, verification fails with `code: INSUFFICIENT_PERMISSIONS`. ## Basic permission check Pass a permission string to verify: ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "key": "sk_...", "permissions": "documents.read" }' ``` ```typescript TypeScript theme={null} try { const { meta, data } = await verifyKey({ key: "sk_...", permissions: "documents.read", }); if (!data.valid) { if (data.code === "INSUFFICIENT_PERMISSIONS") { // Key is valid but lacks the permission return Response.json({ error: "Insufficient permissions" }, { status: 403 }); } return Response.json({ error: data.code }, { status: 401 }); } // Key is valid - continue with your API logic } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Permission query syntax Unkey supports logical operators for complex permission checks: ### Single permission ```json theme={null} { "permissions": "documents.read" } ``` Key must have `documents.read`. ### AND (all required) ```json theme={null} { "permissions": "documents.read AND documents.write" } ``` Key must have **both** permissions. ### OR (any required) ```json theme={null} { "permissions": "admin OR editor" } ``` Key must have **at least one** of the permissions. ### Complex queries with parentheses ```json theme={null} { "permissions": "admin OR (documents.read AND documents.write)" } ``` Key must have `admin` **OR** have both `documents.read` and `documents.write`. ### Real-world example ```typescript theme={null} // User wants to delete a document // They need: admin OR (documents.delete AND owner of this document) try { const { meta, data } = await verifyKey({ key: request.apiKey, permissions: "admin OR documents.delete", }); if (!data.valid) { return Response.json({ error: data.code }, { status: 401 }); } // Key is valid - continue with your API logic } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Response structure Successful verification with permissions: ```json theme={null} { "meta": { "requestId": "req_..." }, "data": { "valid": true, "code": "VALID", "keyId": "key_...", "permissions": ["documents.read", "documents.write", "users.view"] } } ``` Failed permission check: ```json theme={null} { "meta": { "requestId": "req_..." }, "data": { "valid": false, "code": "INSUFFICIENT_PERMISSIONS", "keyId": "key_..." } } ``` ## Manual permission checking Sometimes you need to check permissions after loading data from your database (e.g., checking if the user owns a resource). In these cases: Verify without permission requirements to get the key's permissions list. ```typescript theme={null} try { const { meta, data } = await verifyKey({ key: "sk_...", }); if (!data.valid) { return unauthorized(); } const permissions = data.permissions ?? []; // Continue with your API logic } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` Query your database for the resource. ```typescript theme={null} const document = await db.documents.find(documentId); ``` Use the permissions array and your data to make the decision. ```typescript theme={null} const canDelete = permissions.includes("admin") || (permissions.includes("documents.delete") && document.ownerId === data.identity?.externalId); if (!canDelete) { return forbidden(); } ``` ## Wildcard permissions Permissions support wildcards for broader access: ```typescript theme={null} // Key has: ["documents.*"] // This grants: documents.read, documents.write, documents.delete, etc. try { const { meta, data } = await verifyKey({ key: "sk_...", permissions: "documents.read", // ✅ Passes (matched by documents.*) }); if (!data.valid) { return Response.json({ error: data.code }, { status: 401 }); } // Key is valid - continue with your API logic } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` Common wildcard patterns: * `*` — All permissions (use carefully!) * `documents.*` — All document permissions * `api.v1.*` — All v1 API permissions ## Best practices Instead of `admin`, define specific permissions like `users.delete`, `billing.manage`. This gives you more control and better audit trails. Don't just check in the UI — always verify permissions server-side during API requests. Instead of attaching 10 permissions to every key, create a role and attach that. Easier to manage and update. ## Next steps Create your authorization structure See a complete implementation # Disabling Keys Source: https://unkey.com/docs/apis/features/enabled Temporarily disable keys without deleting them Disabling a key makes it invalid for verification without permanently deleting it. The key can be re-enabled at any time, restoring full access. ## When to use this Customer's payment failed — disable their key until billing is resolved. Investigate potential abuse without losing the key's configuration. Temporarily block access during system updates. Suspend a user's API access while keeping their key for potential reactivation. ## Disable a key ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "enabled": false }' ``` ```typescript TypeScript theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); try { const { meta, data } = await unkey.keys.update({ keyId: "key_...", enabled: false, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Verification response When a disabled key is verified, it returns `valid: false` with code `DISABLED`: ```json theme={null} { "meta": { "requestId": "req_..." }, "data": { "valid": false, "code": "DISABLED", "keyId": "key_...", "enabled": false } } ``` ## Re-enable a key ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "enabled": true }' ``` ```typescript TypeScript theme={null} try { const { meta, data } = await unkey.keys.update({ keyId: "key_...", enabled: true, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Disabled vs Deleted | Action | Disabled | Deleted | | -------------------- | ----------------------- | ------------------------ | | Key verifies? | ❌ No (`code: DISABLED`) | ❌ No (`code: NOT_FOUND`) | | Can be restored? | ✅ Yes | ❌ No | | Keeps configuration? | ✅ Yes | ❌ No | | Keeps analytics? | ✅ Yes | ⚠️ Limited | Use disabling for temporary blocks. Only delete keys when you're sure the user won't need them again. # Environments Source: https://unkey.com/docs/apis/features/environments Separate your keys into live and test environments Environments let you issue distinct keys for development, staging, and production. Test keys can't affect real resources, while live keys have full access. ## When to use this Developers can test integrations without touching production data. CI/CD pipelines can run tests with keys that won't affect live resources. Test keys might have lower rate limits or be free to use. Prefixes like `sk_test_` make it obvious when using non-production keys. ## How it works Environments are implemented using key metadata and prefixes — there's no special "environment" field. This gives you full flexibility to model environments however you want. Common pattern: * **Prefix**: `sk_test_` vs `sk_live_` (visible to users) * **Metadata**: `{ "environment": "test" }` (returned on verification) ## Create environment-specific keys ```bash Test key theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_...", "prefix": "sk_test", "meta": { "environment": "test" }, "ratelimits": [{ "name": "requests", "limit": 100, "duration": 60000 }] }' ``` ```bash Live key theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_...", "prefix": "sk_live", "meta": { "environment": "production" }, "ratelimits": [{ "name": "requests", "limit": 1000, "duration": 60000 }] }' ``` Using prefixes makes it obvious to users which environment a key is for. This prevents accidental use of test keys in production (or worse, live keys in tests). ## Verify and check environment The metadata is returned during verification, so your API can behave differently: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -d '{ "key": "sk_test_abc123..." }' ``` Response: ```json theme={null} { "meta": { "requestId": "req_..." }, "data": { "valid": true, "keyId": "key_...", "meta": { "environment": "test" } } } ``` ## Handle environments in your API ```typescript theme={null} try { const { meta, data } = await verifyKey({ key: request.apiKey }); if (!data.valid) { return unauthorized(); } const environment = data.meta?.environment ?? "production"; if (environment === "test") { // Use test database, sandbox resources, etc. return handleTestRequest(request); } // Production request return handleLiveRequest(request); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Common patterns ### Different rate limits per environment ```typescript theme={null} // Test: 100/min, Live: 10,000/min const ratelimits = environment === "test" ? [{ name: "requests", limit: 100, duration: 60000 }] : [{ name: "requests", limit: 10000, duration: 60000 }]; try { const { meta, data } = await unkey.keys.create({ apiId, prefix: environment === "test" ? "sk_test" : "sk_live", meta: { environment }, ratelimits, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ### Sandbox vs real resources ```typescript theme={null} if (data.meta?.environment === "test") { // Return mock data or use sandbox return { charged: false, response: mockResponse }; } // Real billing, real data try { await chargeUser(data.identity.externalId, amount); return { charged: true, response: realResponse }; } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ### Block test keys from production endpoint ```typescript theme={null} if (data.meta?.environment === "test" && isProductionEndpoint) { return Response.json( { error: "Test keys cannot access production endpoints" }, { status: 403 } ); } // Continue with your API logic ``` ## Next steps Learn about key metadata and features Set different limits per environment # Key Rate Limits Source: https://unkey.com/docs/apis/features/ratelimiting/overview Attach rate limits directly to API keys Unkey's ratelimiting system controls the number of requests a key can make within a given timeframe. This prevents abuse, protects your API from being overwhelmed, and enables usage-based billing models. ## Multi-Ratelimit System Unkey supports multiple named ratelimits per key, providing fine-grained control over different aspects of your API usage. You can define separate limits for different operations, time windows, and use cases within a single key. The system offers two types of ratelimits: auto-apply ratelimits that check automatically during verification, and manual ratelimits that require explicit specification in requests. ## Auto-Apply vs Manual Ratelimits Auto-apply ratelimits (`autoApply: true`) are checked automatically during every key verification without needing explicit specification. These work well for general usage limits that should always be enforced. ```bash theme={null} # Create a key with auto-apply ratelimit curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_123", "ratelimits": [ { "name": "general-requests", "limit": 100, "duration": 60000, "autoApply": true } ] }' ``` ```bash theme={null} # Verify key - auto-apply ratelimits are checked automatically curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "key": "your_api_key_here" }' ``` Manual ratelimits (`autoApply: false`) must be explicitly specified in verification requests. Use these for operation-specific limits that only apply to certain endpoints. ```bash theme={null} # Create a key with manual ratelimit curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_123", "ratelimits": [ { "name": "expensive-operations", "limit": 5, "duration": 60000 } ] }' ``` ```bash theme={null} # Verify key - override cost explicitly curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "key": "your_api_key_here", "ratelimits": [ { "name": "expensive-operations", "cost": 2 } ] }' ``` ## Configuration Configure ratelimits on a per-key or per-identity basis through the dashboard or API. Each ratelimit requires a unique name, limit count, and duration in milliseconds. ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_123", "ratelimits": [ { "name": "requests", "limit": 100, "duration": 60000, "autoApply": true }, { "name": "daily-quota", "limit": 1000, "duration": 86400000, "autoApply": true } ] }' ``` You can apply different costs to operations by specifying a cost parameter during verification. This allows resource-intensive operations to consume more of the rate limit than simple operations. ## Identity-Level Ratelimits Configure ratelimits at the identity level to share limits across multiple keys. Identity ratelimits work alongside key-level ratelimits, with key-level settings taking precedence for naming conflicts. ```bash theme={null} curl -X POST https://api.unkey.com/v2/identities.createIdentity \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "externalId": "user_123", "ratelimits": [ { "name": "user-requests", "limit": 1000, "duration": 3600000 } ] }' ``` This approach works well for user-based quotas where multiple API keys should share the same usage limits. # Auto-Refill Source: https://unkey.com/docs/apis/features/refill Automatically restore credits on a daily or monthly schedule Auto-refill works with [usage limits](/apis/features/remaining) to automatically restore a key's credits on a schedule. Perfect for subscription models where users get a fresh allocation each billing period. ## When to use this "Pro plan: 50,000 requests/month" — credits reset on the 1st of each month. Free tier users get 100 requests per day, resets at midnight UTC. Automatically restore credits without manual intervention. ## How it works 1. You create a key with `credits.remaining` and `credits.refill` configured 2. Unkey automatically refills the credits on your chosen schedule 3. **Daily refills** trigger at midnight UTC 4. **Monthly refills** trigger on a specific day of the month (default: 1st) Refill **replaces** the current credit balance — it doesn't add to it. A key with 50 remaining credits will have exactly 1000 after a refill of 1000, not 1050. ## Create a key with auto-refill ### Monthly refill (most common) Key gets 10,000 credits that reset on the 1st of each month: ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_...", "credits": { "remaining": 10000, "refill": { "interval": "monthly", "amount": 10000 } } }' ``` ```typescript TypeScript theme={null} try { const { meta, data } = await unkey.keys.create({ apiId: "api_...", remaining: 10000, refill: { interval: "monthly", amount: 10000, }, }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ### Monthly refill on a specific day If your billing cycle is mid-month, set `refillDay`: ```json theme={null} { "apiId": "api_...", "credits": { "remaining": 10000, "refill": { "interval": "monthly", "amount": 10000, "refillDay": 15 } } } ``` This refills on the 15th of each month. For months with fewer days (e.g., February), refills on the last day of the month if `refillDay` exceeds the month's length. ### Daily refill Key gets 100 credits that reset every night at midnight UTC: ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_...", "credits": { "remaining": 100, "refill": { "interval": "daily", "amount": 100 } } }' ``` ```typescript TypeScript theme={null} try { const { meta, data } = await unkey.keys.create({ apiId: "api_...", remaining: 100, refill: { interval: "daily", amount: 100, }, }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Update refill settings Change an existing key's refill configuration: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "credits": { "remaining": 50000, "refill": { "interval": "monthly", "amount": 50000 } } }' ``` ## Disable refill To stop auto-refill but keep the current credits: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "credits": { "refill": null } }' ``` ## Common patterns ### Tiered subscription plans ```typescript theme={null} // Free tier: 100/day try { const { meta, data } = await unkey.keys.create({ apiId, remaining: 100, refill: { interval: "daily", amount: 100 }, }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } // Pro tier: 50,000/month try { const { meta, data } = await unkey.keys.create({ apiId, remaining: 50000, refill: { interval: "monthly", amount: 50000 }, }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } // Enterprise: Unlimited (no credits object) try { const { meta, data } = await unkey.keys.create({ apiId }); console.log(data.keyId); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ### Upgrade a user's plan When a user upgrades, update their key: ```typescript theme={null} // User upgraded from Free to Pro mid-month try { const { meta, data } = await unkey.keys.update({ keyId: "key_...", credits: { remaining: 50000, // Give them the new allocation immediately refill: { interval: "monthly", amount: 50000, }, }, }); } catch (error) { console.error(error); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Refill vs manual credit management | Approach | Best for | | -------------------- | ------------------------------------------------------ | | **Auto-refill** | Subscription models with predictable, recurring quotas | | **Manual increment** | Pay-as-you-go, top-ups, credit purchases | You can use both: set a base refill for the subscription, and manually increment when users purchase additional credits. ## Next steps Learn more about credits and custom costs Track usage patterns per key # Usage Limits (Credits) Source: https://unkey.com/docs/apis/features/remaining Cap total API requests per key with automatic enforcement Usage limits let you set a maximum number of requests a key can make. After the limit is reached, the key becomes invalid until you add more credits or reset it. This is different from rate limiting, which controls *frequency* — usage limits control *total volume*. ## When to use this Sell API credits that get consumed with each request. Customer buys 10,000 requests, key stops working at 10,001. Give new users 100 free requests to try your API. They upgrade or stop. "Pro plan includes 50,000 requests/month" — combine with [auto-refill](/apis/features/refill) for recurring limits. Create a key that works exactly once — like a single-use download link. ## How it works 1. Create a key with `credits.remaining` set to your limit 2. Each verification decrements the remaining count 3. When remaining hits 0, verification fails with `code: USAGE_EXCEEDED` 4. You can add more credits anytime via the API ## Create a key with usage limits ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_...", "credits": { "remaining": 1000 } }' ``` ```typescript TypeScript theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); try { const { meta, data } = await unkey.keys.create({ apiId: "api_...", remaining: 1000, }); console.log(data.keyId); // Give this key to your user } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Verification response When you verify a key with usage limits, the response includes the current credit count: ```json theme={null} { "meta": { "requestId": "req_..." }, "data": { "valid": true, "code": "VALID", "keyId": "key_...", "credits": 999 } } ``` The `credits` value shows remaining credits **after** this verification. A value of `999` means the key can be verified 999 more times. When credits are exhausted: ```json theme={null} { "meta": { "requestId": "req_..." }, "data": { "valid": false, "code": "USAGE_EXCEEDED", "keyId": "key_...", "credits": 0 } } ``` ## Custom cost per request By default, each verification costs 1 credit. But some operations should cost more — maybe a complex query costs 10 credits while a simple lookup costs 1. Specify the cost at verification time: ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -d '{ "key": "sk_...", "credits": { "cost": 10 } }' ``` ```typescript TypeScript theme={null} try { const { meta, data } = await verifyKey({ key: "sk_...", apiId: "api_...", cost: 10, // Deduct 10 credits for this request }); if (!data.valid) { return Response.json({ error: data.code }, { status: 401 }); } } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` If the key doesn't have enough remaining credits, the verification fails. A key with 5 remaining credits will reject a request with `cost: 10`. ### Cost of 0 (check without consuming) Set `cost: 0` to verify a key without consuming any credits. Useful for checking key validity or metadata without affecting the balance. ```json theme={null} { "key": "sk_...", "credits": { "cost": 0 } } ``` ## Add more credits When a user purchases more credits or you need to refill manually: ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.updateCredits \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "operation": "increment", "value": 5000 }' ``` ```typescript TypeScript theme={null} try { const { meta, data } = await unkey.keys.updateCredits({ keyId: "key_...", operation: "increment", value: 5000, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` Operations available: * `set` — Set credits to an exact value * `increment` — Add credits to current balance * `decrement` — Subtract credits from current balance ## Remove usage limits To make a key unlimited again, set credits to `null`: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "credits": null }' ``` ## Usage limits vs rate limits | Feature | Usage Limits | Rate Limits | | --------------------- | ------------------------ | ---------------------------- | | What it controls | Total requests ever | Requests per time window | | Resets automatically? | No (unless using refill) | Yes, after window expires | | Use case | Billing, quotas, trials | Abuse protection, fair usage | | Example | "1000 requests total" | "100 requests per minute" | **Use both together:** Rate limits protect against burst abuse, usage limits enforce billing quotas. ## Next steps Automatically reset credits on a schedule (daily, monthly) Control request frequency, not just total volume # Key Rerolling Source: https://unkey.com/docs/apis/features/rerolling-key Rotate API keys while preserving their configuration and permissions ## What is Key Rerolling? Key rerolling (or key rotation) is the process of generating a new API key token while preserving all the configuration from an existing key. This is a critical security practice that allows you to regularly rotate credentials without disrupting your application's permissions or settings. ## Why Reroll Keys? Key rerolling serves several important purposes: * **Security Compliance**: Many security frameworks require regular credential rotation * **Compromise Recovery**: Quickly replace keys that may have been exposed * **Proactive Security**: Regularly rotate keys as a preventive measure * **Graceful Migration**: Overlap periods allow zero-downtime key transitions ## How Key Rerolling Works ### What Gets Copied When you reroll a key, the new key is an exact copy of the original in terms of configuration: **Preserved Settings:** * Permissions and RBAC roles * Custom metadata fields * Rate limiting rules * Identity associations (for tracking usage across keys) * Remaining credits balance * Recovery/encryption settings * API association ### What's New The rerolled key gets fresh values for: * Key ID (a new unique identifier) * API key token (the actual secret) * Creation timestamp ### What Happens to the Original Key The original key remains active for a configurable grace period: * You specify the `expiration` duration (in milliseconds) * Set to `0` for immediate revocation * Common grace periods: 1 hour (3600000ms), 24 hours (86400000ms), 7 days (604800000ms) ## Making a Reroll Request To reroll a key, make a `POST` request to `/v2/keys.rerollKey`: ```bash theme={null} curl --request POST \ --url https://api.unkey.com/v2/keys.rerollKey \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q", "expiration": 86400000 }' ``` ### Request Parameters * `keyId` (required): The database identifier of the key to reroll (NOT the API key token) * `expiration` (required): Duration in milliseconds until the original key is revoked ### Response ```json theme={null} { "meta": { "requestId": "req_abc123def456" }, "data": { "keyId": "key_3dHLcNyRzJaiDyo2Jekz9r", "key": "prod_2cGKbMxRjIzhCxo1IdjH3arELti7Sdyc8w6XYbvtcyuBowPT" } } ``` **Security Critical**: The `key` field contains the actual API key token. This is the only time you'll receive it - Unkey stores only a hashed version. Never log or expose this value. Transmit it directly to the end user via secure channels only. ## Common Use Cases ### Zero-Downtime Key Rotation For production systems that can't afford downtime: 1. Reroll the key with a grace period (e.g., 24 hours) 2. Deploy the new key to your systems 3. Verify the new key is working 4. The old key automatically expires after the grace period ```bash theme={null} # Give 24 hours for migration curl --request POST \ --url https://api.unkey.com/v2/keys.rerollKey \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q", "expiration": 86400000 }' ``` ### Emergency Key Replacement When a key is compromised and needs immediate revocation: ```bash theme={null} # Revoke immediately curl --request POST \ --url https://api.unkey.com/v2/keys.rerollKey \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "keyId": "key_2cGKbMxRyIzhCxo1Idjz8q", "expiration": 0 }' ``` ## Analytics and Usage Tracking An important aspect of key rerolling is that analytics remain consistent: * **Key-level metrics**: Each key has its own usage statistics * **Identity-level metrics**: If the original key has an identity, the new key inherits it * This allows you to track usage across both individual keys and the overall identity * Historical data from the original key remains accessible ## Required Permissions Your root key needs the following permissions to reroll keys: * `api.*.create_key` or `api..create_key` * `api.*.encrypt_key` or `api..encrypt_key` (only when the original key is recoverable) ## Limitations * The new key uses the API's default configuration for prefix and byte length * You cannot modify permissions or settings during reroll - use the update endpoint afterward if needed # Key Revocation Source: https://unkey.com/docs/apis/features/revocation Delete or disable keys instantly when needed When a key is compromised or a user's access should end, you can revoke it immediately. Unkey supports both permanent deletion and temporary disabling. ## When to use this Key was leaked in a public repo or logs. Delete it immediately. Customer canceled or employee left. Revoke their API access. Unusual traffic patterns. Disable the key while investigating. Payment failed. Temporarily disable until resolved. ## Delete vs Disable | Action | Effect | Reversible? | Use when | | ----------- | --------------------------------- | ----------- | ----------------------------------- | | **Delete** | Permanently removes the key | No | Key is compromised, user churned | | **Disable** | Key exists but can't authenticate | Yes | Temporary suspension, investigation | ## Delete a key permanently Use when the key should never work again: ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.deleteKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_..." }' ``` ```typescript TypeScript theme={null} try { const { meta, data } = await unkey.keys.delete({ keyId: "key_...", }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` The key is invalidated within 60 seconds globally. Deletion is permanent. The key cannot be recovered. If you might need to restore access, use disable instead. ## Disable a key temporarily Use when you want to suspend access but may restore it later: ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "enabled": false }' ``` ```typescript TypeScript theme={null} try { const { meta, data } = await unkey.keys.update({ keyId: "key_...", enabled: false, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` Verification response when disabled: ```json theme={null} { "data": { "valid": false, "code": "DISABLED", "keyId": "key_..." } } ``` ## Re-enable a disabled key ```typescript theme={null} try { const { meta, data } = await unkey.keys.update({ keyId: "key_...", enabled: true, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` The key works again immediately. ## Propagation time * **Delete**: Up to 60 seconds for global invalidation * **Disable**: Up to 60 seconds for global propagation For immediate revocation of a compromised key, you may want to also: 1. Rotate any affected secrets downstream 2. Review audit logs for unauthorized access 3. Alert the user if appropriate ## Bulk revocation To revoke all keys for a specific user, query their keys first: ```typescript theme={null} try { // Get all keys for a user const { meta, data } = await unkey.keys.list({ apiId: "api_...", externalId: "user_123", }); // Delete them all for (const key of data.keys) { await unkey.keys.delete({ keyId: key.id }); } } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` For security incidents, consider using [key rerolling](/apis/features/rerolling-key) if you need to maintain the user's configuration while replacing the compromised key. ## Next steps Replace a key while preserving its configuration Track who revoked which keys # Temporary Keys Source: https://unkey.com/docs/apis/features/temp-keys Create API keys that automatically expire after a set time Temporary keys automatically become invalid after a specified time. No cleanup required — Unkey handles expiration and deletion for you. ## When to use this Give new users a 7-day trial key. It stops working automatically. Partner needs API access for a project? Key expires when the project ends. Generate a key that lasts for a user's session (e.g., 24 hours). Force key rotation by having keys expire periodically. ## How it works 1. Create a key with an `expires` timestamp (Unix milliseconds) 2. Key works normally until the expiration time 3. After expiration, verification returns `code: EXPIRED` 4. Unkey automatically cleans up expired keys ## Create a temporary key Set `expires` to a Unix timestamp in **milliseconds**: ```bash cURL theme={null} # Key expires in 24 hours EXPIRES=$(($(date +%s) * 1000 + 86400000)) curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_...", "expires": '$EXPIRES' }' ``` ```typescript TypeScript theme={null} // Key expires in 24 hours const expires = Date.now() + 24 * 60 * 60 * 1000; try { const { meta, data } = await unkey.keys.create({ apiId: "api_...", expires, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ```python Python theme={null} import time # Key expires in 24 hours expires = int(time.time() * 1000) + (24 * 60 * 60 * 1000) result = unkey.keys.create_key( api_id="api_...", expires=expires ) ``` ## Common expiration times | Duration | Calculation | | ------------- | --------------------------------------- | | 1 hour | `Date.now() + 60 * 60 * 1000` | | 24 hours | `Date.now() + 24 * 60 * 60 * 1000` | | 7 days | `Date.now() + 7 * 24 * 60 * 60 * 1000` | | 30 days | `Date.now() + 30 * 24 * 60 * 60 * 1000` | | Specific date | `new Date("2024-12-31").getTime()` | ## Verification response When verifying an expired key: ```json theme={null} { "meta": { "requestId": "req_..." }, "data": { "valid": false, "code": "EXPIRED", "keyId": "key_..." } } ``` ## Extend or change expiration Update the expiration time on an existing key: ```bash cURL theme={null} # Extend by 7 more days from now EXPIRES=$(($(date +%s) * 1000 + 604800000)) curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "expires": '$EXPIRES' }' ``` ```typescript TypeScript theme={null} // Extend by 7 more days from now try { const { meta, data } = await unkey.keys.update({ keyId: "key_...", expires: Date.now() + 7 * 24 * 60 * 60 * 1000, }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Remove expiration Make a temporary key permanent by setting `expires` to `null`: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.updateKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "keyId": "key_...", "expires": null }' ``` ## Combining with other features Temporary keys work with all other key features: ```typescript theme={null} // 7-day trial with 1000 requests and rate limiting try { const { meta, data } = await unkey.keys.create({ apiId: "api_...", expires: Date.now() + 7 * 24 * 60 * 60 * 1000, credits: { remaining: 1000, }, ratelimits: [{ name: "requests", limit: 100, duration: 60000, // 100 per minute }], }); } catch (err) { console.error(err); return Response.json({ error: "Internal error" }, { status: 500 }); } ``` ## Temporary vs usage-limited keys | Feature | Temporary Keys | Usage-Limited Keys | | ------------- | -------------------- | --------------------- | | Controlled by | Time | Request count | | Expires when | Clock hits timestamp | Credits reach 0 | | Good for | Time-bound access | Pay-per-use, quotas | | Example | "Access for 7 days" | "1000 requests total" | **Use both together** for trial limits: "7 days OR 1000 requests, whichever comes first." ## Next steps Cap total requests per key Generate new keys while maintaining config # IP Whitelisting Source: https://unkey.com/docs/apis/features/whitelist Restrict API key usage to specific IP addresses IP whitelisting lets you restrict which IP addresses can use a key. Even with a valid key, requests from non-whitelisted IPs are rejected. This feature is available as an addon or with an Enterprise plan. [Contact us](https://unkey.com/contact) to enable it. ## When to use this Keys should only work from your customer's known server IPs. Restrict partner keys to their office or datacenter IPs. Ensure internal API keys only work from corporate network. Meet security requirements that mandate IP-based access control. ## How it works 1. Configure allowed IP addresses or CIDR ranges on a key 2. When the key is verified, Unkey checks the request's source IP 3. If the IP isn't in the whitelist, verification fails with `code: FORBIDDEN` IP Whitelist example ## Configuration IP whitelisting is configured through the dashboard: Supports: * Individual IPv4 addresses: `192.168.1.100` * IPv4 CIDR ranges: `10.0.0.0/8` * IPv6 addresses and ranges: `2001:db8::/32` ## Verification response When a request comes from a non-whitelisted IP: ```json theme={null} { "data": { "valid": false, "code": "FORBIDDEN", "keyId": "key_..." } } ``` ## Combining with other security features IP whitelisting works alongside other key features: * **Rate limiting**: Still applies after IP check passes * **Permissions**: Authorization checks happen after IP verification * **Expiration**: Key must be valid AND IP must be whitelisted ## Best practices Instead of listing individual IPs, use CIDR notation (`10.0.0.0/24`) so customers can add servers without updating the whitelist. Let users know they need to provide IPs when requesting keys. Dynamic IPs won't work reliably. For users with dynamic IPs, you might offer keys without IP restrictions but with stricter rate limits. ## Next steps Add another layer of protection Enable IP whitelisting for your account # API Key Management Source: https://unkey.com/docs/apis/introduction Issue, verify, and manage API keys at scale API keys are the most common way to authenticate requests to your API. Unkey handles the entire lifecycle — creating keys, verifying them on every request, tracking usage, and revoking them when needed — so you can focus on your actual API logic. ## When to use API keys API keys work best when you need: * **Machine-to-machine authentication** — Backend services, scripts, or integrations calling your API * **Usage tracking per customer** — Know who's making requests and how many * **Simple integration** — A single header (`Authorization: Bearer sk_...`) that developers understand * **Granular control** — Per-key rate limits, expiration, and permissions For user-facing authentication (login flows, sessions), you'll typically use OAuth, JWTs, or an auth provider like Clerk or Auth0 alongside Unkey. Unkey handles the API key layer, not user sessions. ## How it works An "API" in Unkey is a container for keys. You might have separate APIs for "Production" and "Staging", or for different products. When a user signs up or requests API access, create a key for them. You can attach metadata (user ID, plan tier), set limits, and configure expiration. When a request hits your API, extract the key from the header and verify it with Unkey. We'll tell you if it's valid, who it belongs to, and how many requests they have left. Revoke keys when users churn. Rotate keys when security requires it. Update limits when users upgrade. All through the dashboard or API. ## Quick example Here's what verification looks like in practice: ```ts TypeScript theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function handler(req: Request) { // 1. Extract the key from the Authorization header const key = req.headers.get("Authorization")?.replace("Bearer ", ""); if (!key) { return new Response("Missing API key", { status: 401 }); } try { // 2. Verify with Unkey const { meta, data } = await unkey.keys.verifyKey({ key }); if (!data.valid) { // Key is invalid, expired, rate limited, or has no remaining uses return new Response("Unauthorized", { status: 401 }); } // Continue with your API logic... return new Response("Hello!"); } catch (err) { console.error(err); return new Response("Service unavailable", { status: 503 }); } } ``` ```python Python theme={null} from unkey import Unkey unkey = Unkey(root_key="unkey_...") def handler(request): key = request.headers.get("Authorization", "").replace("Bearer ", "") if not key: return Response("Missing API key", status=401) result = unkey.keys.verify_key(key=key) if not result.valid: return Response("Unauthorized", status=401) # Key is valid — continue with your API logic return Response("Hello!") ``` ```go Go theme={null} package main import ( "context" "net/http" "os" "strings" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/components" ) func handler(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") key := strings.TrimPrefix(strings.TrimSpace(auth), "Bearer ") client := unkey.New(unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY"))) res, err := client.Keys.VerifyKey(context.Background(), components.V2KeysVerifyKeyRequestBody{ Key: key, }) if err != nil || !res.Valid { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } w.Write([]byte("Hello!")) } ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{"key": "sk_1234..."}' ``` ## What you get with each verification When you verify a key, Unkey returns: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ## Features Unkey keys support much more than basic authentication: Limit requests per key — per second, minute, hour, or any window. Cap total requests per key. Perfect for API credits or trial limits. Automatically restore usage limits on a schedule (daily, monthly, etc). Create keys that automatically expire after a set time. Attach roles and permissions for fine-grained access control. See usage patterns, top consumers, and verification trends. ## Next steps Full walkthrough for Next.js, Bun, Express, or Hono. All endpoints for creating, updating, and managing keys. # Overview Source: https://unkey.com/docs/audit-log/introduction Track every change to your API keys, permissions, and configuration Audit logs capture every mutation in your workspace — key creation, permission changes, revocations, and more. They're essential for security compliance, debugging, and understanding who changed what and when. ## Why audit logs matter When something goes wrong, trace exactly what happened and who was responsible. SOC 2, HIPAA, and other frameworks require audit trails for sensitive operations. Know when team members create, modify, or revoke keys — especially in shared workspaces. Figure out why a key stopped working or when a permission was changed. ## What's captured Every mutation is logged automatically: | Category | Events | | --------------- | ------------------------------------------------------ | | **Keys** | Create, update, revoke, delete, verify (failures) | | **APIs** | Create, update, delete | | **Permissions** | Create, update, delete, attach to key, remove from key | | **Roles** | Create, update, delete, assign, unassign | | **Rate limits** | Create namespace, update limits, set overrides | | **Workspace** | Member added, member removed, settings changed | ## Accessing audit logs 1. Sign into the [Dashboard](https://app.unkey.com/audit) 2. Click **Audit Logs** in the left navigation ## Reading the log Each entry shows: | Field | Description | | --------------- | ---------------------------------------------------------- | | **Time** | When the change occurred | | **Actor** | Who made the change — a user (dashboard) or root key (API) | | **Action** | The operation: `Create`, `Update`, `Delete`, etc. | | **Event** | What was affected: key, permission, API, etc. | | **Description** | Human-readable summary | Click any row to see full details, including the complete request/response payloads: ## Filtering logs Use the filters at the top of the audit log page to narrow down: * **Event type** — Show only key events, permission events, etc. * **Actor** — Filter by specific user or root key * **Time range** — Focus on a specific period ## Retention Audit logs are retained based on your plan: | Plan | Retention | | ---------- | ------------------------ | | Free | 7 days | | Pro | 90 days | | Enterprise | Custom (up to unlimited) | Need longer retention? [Contact us](mailto:support@unkey.com) about enterprise options. ## Next steps Full list of audited events and their payloads How Unkey protects your data # Event Types Source: https://unkey.com/docs/audit-log/types Available audit log event types ## Workspaces Workspace actions that will create a new audit log item. * `workspace.create` - A workspace is created. * `workspace.update` - A workspace is updated. * `workspace.delete` - A workspace is deleted. * `workspace.opt_in` - A workspace was opted into a feature. ## API API actions that will create a new audit log item. * `api.create` - An API is created. * `api.update` - An API is updated. * `api.delete` - An API is deleted. ## Keys Key actions that will create a new audit log item. * `key.create` - A key is created. * `key.update` - A key is updated. * `key.delete` - A key is deleted. * `key.reroll` - A key is rerolled. ### Ratelimit Namespace Ratelimit Namespace actions that will create a new audit log item. * `ratelimitNamespace.create` - A ratelimit namespace is created. * `ratelimitNamespace.update` - A ratelimit namespace is updated. * `ratelimitNamespace.delete` - A ratelimit namespace is deleted. ### Ratelimit Ratelimit actions that will create a new audit log item. * `ratelimit.create` - A ratelimit is created. * `ratelimit.update` - A ratelimit is updated. * `ratelimit.delete` - A ratelimit is deleted. * `ratelimit.set_override` - A ratelimit override is set. * `ratelimit.read_override` - A ratelimit override is read. * `ratelimit.delete_override` - A ratelimit override is deleted. ## Role Role actions that will create a new audit log item. * `role.create` - A role is created. * `role.update` - A role is updated. * `role.delete` - A role is deleted. ## Permission Permission actions that will create a new audit log item. * `permission.create` - A permission is created. * `permission.update` - A permission is updated. * `permission.delete` - A permission is deleted. ## Authorization Authorization actions that will create a new audit log item. * `authorization.connect_role_and_permission` - A role and permission are connected. * `authorization.disconnect_role_and_permissions` - A role and permission are disconnected. * `authorization.connect_role_and_key` - A role and key are connected. * `authorization.disconnect_role_and_key` - A role and key are disconnected. * `authorization.connect_permission_and_key` - A permission and key are connected. * `authorization.disconnect_permission_and_key` - A permission and key are disconnected. ## Webhook Webhook actions that will create a new audit log item. * `webhook.create` - A webhook is created. * `webhook.update` - A webhook is updated. * `webhook.delete` - A webhook is deleted. ## Identity Identity actions that will create a new audit log item. * `identity.create` - An identity is created. * `identity.update` - An identity is updated. * `identity.delete` - An identity is deleted. # Overview Source: https://unkey.com/docs/concepts/identities/overview Group multiple keys under a user, organization, or any entity in your system Identities let you connect multiple API keys to a single entity — a user, an organization, a service account, or anything else in your system. Once connected, keys can share configuration, metadata, and rate limits. ## Why use identities? Without identities, each API key is standalone. If a user has 3 keys, you need to update each one individually. With identities: | Without Identities | With Identities | | ----------------------------------------- | ------------------------------------------------ | | Update rate limits on each key separately | Update once on the identity, applies to all keys | | Metadata stored per-key | Shared metadata across all keys | | Analytics scattered across keys | Aggregated analytics per identity | | No concept of "this user's total usage" | Shared rate limit pool across all keys | ## Real-world example Imagine you're building a developer platform like Stripe. Each customer might have: * A **test key** for development * A **production key** for live traffic * **Multiple restricted keys** for different services All these keys belong to the same customer. With identities: ```ts theme={null} // Create an identity for the customer try { const { meta, data, error } = await unkey.identities.create({ externalId: "customer_acme_123", // Your internal customer ID meta: { plan: "pro", company: "Acme Corp" } }); if (error) throw error; // All keys for this customer share the identity await unkey.keys.create({ apiId: "api_xxx", identityId: data.id, name: "Production Key" }); await unkey.keys.create({ apiId: "api_xxx", identityId: data.id, name: "Test Key" }); } catch (err) { console.error(err); throw err; } ``` Now when you verify any of these keys, you get the identity metadata: ```ts theme={null} const { meta, data } = await unkey.keys.verify({ key: "sk_..." }); console.log(data.identity); // { id: "id_xxx", externalId: "customer_acme_123", meta: { plan: "pro", company: "Acme Corp" } } ``` ## Use cases Set a rate limit on the identity, and all associated keys share the same pool. If a user has 1000 requests/hour across all their keys, they can't bypass the limit by using different keys. [Learn more →](/concepts/identities/ratelimits) Instead of piecing together usage from individual keys, get total usage per identity. Perfect for billing dashboards and customer insights. Store plan tier, feature flags, or custom limits on the identity. All keys inherit this configuration without needing individual updates. Map identities to organizations in your system. When an org adds team members or creates new keys, they all share the same identity and limits. ## Data model Unkey's internal identifier for this identity. Use this to reference the identity in API calls. **Your** identifier for this entity. This links the Unkey identity to your system — typically a user ID, org ID, or customer ID. If you use Clerk, Auth0, WorkOS, or similar: use their user/org ID as the externalId. ```ts theme={null} // Example with Clerk const identity = await unkey.identities.create({ externalId: clerkUserId, // "user_2NNEqL2nrIRdJ194ndJqAHwEfxC" }); ``` Arbitrary JSON metadata. Store anything useful: plan tier, feature flags, company name, etc. ```json theme={null} { "plan": "enterprise", "features": ["advanced-analytics", "sso"], "billingEmail": "billing@acme.com" } ``` ## Next steps Enforce limits across all keys for an identity Step-by-step guide to implementing identities # Shared Rate Limits Source: https://unkey.com/docs/concepts/identities/ratelimits Share rate limits across multiple API keys using identities Identities let you enforce a **single rate limit** across all of a user's API keys. Without this, a user with 5 keys at 100 req/s each could make 500 req/s total — probably not what you want. ## The problem Without shared limits, rate limits apply per-key: ``` User has 3 API keys, each with 100 req/s limit → User can actually make 300 req/s (100 × 3 keys) ``` ## The solution Create an identity for the user, attach a rate limit to it, then link all their keys to that identity: ``` Identity: user_123 └── Rate limit: 100 req/s (shared pool) ├── Key A (production) ├── Key B (staging) └── Key C (mobile app) → User can only make 100 req/s total across ALL keys ``` ## Step 1: Create an identity with rate limits ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/identities.createIdentity \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "externalId": "user_123", "ratelimits": [ { "name": "requests", "limit": 100, "duration": 1000 } ] }' ``` ```typescript TypeScript theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); const { meta, data } = await unkey.identities.create({ externalId: "user_123", ratelimits: [ { name: "requests", limit: 100, duration: 1000, // 100 requests per second }, ], }); console.log(data.identityId); // Save this ``` ## Step 2: Create keys linked to the identity ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_...", "externalId": "user_123", "name": "Production Key" }' ``` ```typescript TypeScript theme={null} // All keys with the same externalId share the identity's rate limits try { const { meta, data } = await unkey.keys.create({ apiId: "api_...", externalId: "user_123", // Links to the identity name: "Production Key", }); await unkey.keys.create({ apiId: "api_...", externalId: "user_123", // Same identity name: "Staging Key", }); } catch (err) { console.error(err); throw err; } ``` ## Step 3: Verify — limits are shared automatically When you verify any key linked to the identity, the shared rate limit is enforced: ```typescript theme={null} const { meta, data } = await verifyKey({ key: "sk_prod_..." }); // If the identity is rate limited, data.valid will be false if (!data.valid && data.code === "RATE_LIMITED") { // User exceeded 100 req/s across ALL their keys } ``` *** ## Multiple rate limits You can set multiple limits on an identity — useful for different resource types: ```typescript theme={null} try { const { meta, data } = await unkey.identities.create({ externalId: "user_123", ratelimits: [ { name: "requests", limit: 500, duration: 3600000, // 500 requests per hour }, { name: "tokens", limit: 20000, duration: 86400000, // 20k tokens per day }, ], }); } catch (err) { console.error(err); throw err; } ``` Then specify which limit to check during verification: ```typescript theme={null} // Check the "tokens" limit and consume 150 tokens const { meta, data } = await verifyKey({ key: "sk_...", ratelimits: [ { name: "tokens", cost: 150 }, ], }); ``` If *any* specified limit is exceeded, verification fails. *** ## Next steps Step-by-step tutorial Learn more about identities # Cloudflare Workers Source: https://unkey.com/docs/cookbook/cloudflare-workers API key authentication at the edge with Cloudflare Workers Protect your Cloudflare Workers with Unkey's globally distributed verification. ## Install ```bash theme={null} npm create cloudflare@latest my-api cd my-api npm install @unkey/api ``` ## Basic Worker ```typescript src/index.ts theme={null} import { Unkey } from "@unkey/api"; export interface Env { UNKEY_ROOT_KEY: string; } export default { async fetch(request: Request, env: Env): Promise { // 1. Extract API key const authHeader = request.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { return Response.json( { error: "Missing API key" }, { status: 401 } ); } const unkey = new Unkey({ rootKey: env.UNKEY_ROOT_KEY }); const apiKey = authHeader.slice(7); try { // 2. Verify with Unkey const { meta, data, error } = await unkey.keys.verifyKey({ key: apiKey, }); // 3. Handle errors if (error) { console.error("Unkey error:", error); return Response.json( { error: "Authentication service unavailable" }, { status: 503 } ); } // 4. Check validity if (!data.valid) { return Response.json( { error: data.code }, { status: data.code === "RATE_LIMITED" ? 429 : 401 } ); } // 5. Request is authenticated return Response.json({ message: "Access granted", user: data.identity?.externalId, remaining: data.credits, }); } catch (error) { console.error("Unkey error:", error); return Response.json( { error: "Authentication service unavailable" }, { status: 503 } ); } }, }; ``` ## Configure Secrets ```bash theme={null} npx wrangler secret put UNKEY_ROOT_KEY # Enter your API ID when prompted ``` *** ## With Hono For a cleaner routing experience, use Hono: ```bash theme={null} npm install hono @unkey/hono ``` ```typescript src/index.ts theme={null} import { Hono } from "hono"; import { unkey } from "@unkey/hono"; type Bindings = { UNKEY_ROOT_KEY: string; }; const app = new Hono<{ Bindings: Bindings }>(); // Public routes app.get("/health", (c) => c.json({ status: "ok" })); // Protected routes with Unkey middleware app.use("/api/*", async (c, next) => { const handler = unkey({ rootKey: c.env.UNKEY_ROOT_KEY, getKey: (c) => c.req.header("Authorization")?.replace("Bearer ", ""), onError: (c, error) => { console.error("Unkey error:", error); return c.json({ error: "Service unavailable" }, 503); }, handleInvalidKey: (c, result) => { return c.json({ error: result.code }, 401); }, }); return handler(c, next); }); app.get("/api/data", (c) => { const auth = c.get("unkey"); return c.json({ message: "Access granted", user: auth.identity?.externalId, remaining: auth.credits, }); }); app.get("/api/users", (c) => { return c.json({ users: [] }); }); export default app; ``` *** ## Reusable Auth Middleware Create a clean middleware pattern: ```typescript src/middleware/auth.ts theme={null} import { Unkey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components"; import { Context, Next } from "hono"; declare module "hono" { interface ContextVariableMap { auth: V2KeysVerifyKeyResponseData; } } interface AuthOptions { getKey?: (c: Context) => string | null; permissions?: string; } type Bindings = { UNKEY_ROOT_KEY: string; }; export function authMiddleware(options: AuthOptions = {}) { return async (c: Context, next: Next) => { const getKey = options.getKey ?? ((c) => c.req.header("Authorization")?.replace("Bearer ", "") ?? null ); const apiKey = getKey(c); if (!apiKey) { return c.json({ error: "Missing API key" }, 401); } try { const unkey = new Unkey({ apiKey: c.env.UNKEY_ROOT_KEY }); const { data, error } = await unkey.verifyKey({ key: apiKey, permissions: options.permissions, }); if (error) { console.error("Unkey error:", error); return c.json({ error: "Service unavailable" }, 503); } if (!data.valid) { if (data.code === "INSUFFICIENT_PERMISSIONS") { return c.json({ error: "Forbidden" }, 403); } return c.json({ error: data.code }, { status: 401 }); } c.set("auth", data); await next(); } catch (err) { return c.json({ error: "Service unavailable" }, 503); } }; } ``` Use it: ```typescript src/index.ts theme={null} import { Hono } from "hono"; import { authMiddleware } from "./middleware/auth"; const app = new Hono(); // Basic auth app.use("/api/*", (c, next) => authMiddleware()(c, next) ); // Permission-based auth for admin routes app.use("/admin/*", (c, next) => authMiddleware({ permissions: "admin", })(c, next) ); app.get("/api/data", (c) => { const auth = c.get("auth"); return c.json({ user: auth.identity?.externalId }); }); app.delete("/admin/users/:id", (c) => { // Only accessible with admin permission return c.json({ deleted: c.req.param("id") }); }); export default app; ``` *** ## Rate Limit Headers Add standard rate limit headers: ```typescript src/middleware/auth.ts theme={null} import { Unkey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components"; export function authMiddleware(options: AuthOptions = {}) { return async (c: Context, next: Next) => { const apiKey = c.req.header("Authorization")?.replace("Bearer ", ""); if (!apiKey) { return c.json({ error: "Missing API key" }, 401); } try { const unkey = new Unkey({ apiKey: c.env.UNKEY_ROOT_KEY }); const { meta, data, error } = await unkey.verifyKey({ key: apiKey, }); // Set rate limit headers (v2 uses ratelimits array) if (data.ratelimits?.[0]) { const rl = data.ratelimits[0]; c.header("X-RateLimit-Limit", rl.limit.toString()); c.header("X-RateLimit-Remaining", rl.remaining.toString()); c.header("X-RateLimit-Reset", rl.reset.toString()); } if (data.credits !== undefined) { c.header("X-Credits-Remaining", data.credits.toString()); } if (!data.valid) { const status = data.code === "RATE_LIMITED" ? 429 : 401; return c.json({ error: data.code }, status); } c.set("auth", data); await next(); } catch (err) { return c.json({ error: "Service unavailable" }, 503); } }; } ``` *** ## With Durable Objects For stateful applications with Durable Objects: ```typescript src/index.ts theme={null} import { Unkey } from "@unkey/api"; import { DurableObject } from "cloudflare:workers"; export interface Env { UNKEY_ROOT_KEY: string; } export class Counter extends DurableObject { async fetch(request: Request) { let count = (await this.ctx.storage.get("count")) as number || 0; count++; await this.ctx.storage.put("count", count); return Response.json({ count }); } } export default { async fetch(request: Request, env: Env): Promise { const unkey = new Unkey({ rootKey: env.UNKEY_ROOT_KEY }); // Verify API key first const apiKey = request.headers.get("Authorization")?.slice(7); if (!apiKey) { return Response.json({ error: "Missing API key" }, { status: 401 }); } try { const { meta, data, error } = await unkey.keys.verifyKey({ key: apiKey, }); if (error || !data.valid) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } // Use external ID as Durable Object ID for per-user state const id = env.COUNTER.idFromName(data.identity?.externalId ?? "anonymous"); const counter = env.COUNTER.get(id); return counter.fetch(request); } catch (error) { return Response.json({ error: "Service unavailable" }, { status: 503 }); } }, }; ``` *** ## wrangler.toml ```toml wrangler.toml theme={null} name = "my-api" main = "src/index.ts" compatibility_date = "2024-01-01" # Store UNKEY_ROOT_KEY as a secret, not in config # npx wrangler secret put UNKEY_ROOT_KEY ``` *** ## Deploy ```bash theme={null} npx wrangler deploy ``` Your API is now protected globally at the edge! 🌍 # Endpoint-Specific Rate Limits Source: https://unkey.com/docs/cookbook/endpoint-ratelimit Apply different rate limits to different API endpoints Not all endpoints are equal. Your `/health` endpoint can handle thousands of requests, but your `/ai/generate` endpoint calls an expensive LLM. This recipe shows how to apply different rate limits per endpoint. ## The pattern ```typescript theme={null} // Define limits per endpoint pattern const ENDPOINT_LIMITS = { "/api/ai/*": { limit: 10, duration: "1m" }, // Expensive AI calls "/api/export/*": { limit: 5, duration: "1h" }, // Heavy data exports "/api/*": { limit: 100, duration: "1m" }, // Default API routes }; // Match request path to limits function getLimits(path: string) { for (const [pattern, config] of Object.entries(ENDPOINT_LIMITS)) { if (matchPath(pattern, path)) return config; } return { limit: 100, duration: "1m" }; // fallback } ``` ## Full implementation ### Next.js Middleware ```typescript theme={null} // middleware.ts import { Ratelimit } from "@unkey/ratelimit"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1m", }); // Define endpoint-specific limits (most specific first) const ENDPOINT_LIMITS: Array<{ pattern: RegExp; limit: number; duration: string; namespace: string; }> = [ // Expensive AI endpoints - very tight limits { pattern: /^\/api\/ai\/.*/, limit: 10, duration: "1m", namespace: "ai", }, // Data export - limit per hour { pattern: /^\/api\/export\/.*/, limit: 5, duration: "1h", namespace: "export", }, // Write operations - moderate limits { pattern: /^\/api\/.*\/(create|update|delete)/, limit: 30, duration: "1m", namespace: "writes", }, // Default API routes { pattern: /^\/api\/.*/, limit: 100, duration: "1m", namespace: "api", }, ]; function getEndpointConfig(pathname: string) { for (const config of ENDPOINT_LIMITS) { if (config.pattern.test(pathname)) { return config; } } return ENDPOINT_LIMITS[ENDPOINT_LIMITS.length - 1]; // default } export async function middleware(request: NextRequest) { // Skip non-API routes if (!request.nextUrl.pathname.startsWith("/api")) { return NextResponse.next(); } const userId = request.headers.get("x-user-id") ?? request.ip ?? "anonymous"; const config = getEndpointConfig(request.nextUrl.pathname); // Use endpoint-specific namespace for separate counters const { success, remaining, reset } = await limiter.limit( `${config.namespace}:${userId}`, { limit: config.limit, duration: config.duration as any, } ); if (!success) { return NextResponse.json( { error: "Rate limit exceeded", endpoint: config.namespace, retryAfter: Math.ceil((reset - Date.now()) / 1000), }, { status: 429, headers: { "X-RateLimit-Limit": config.limit.toString(), "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": reset.toString(), "Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(), }, } ); } const response = NextResponse.next(); response.headers.set("X-RateLimit-Limit", config.limit.toString()); response.headers.set("X-RateLimit-Remaining", remaining.toString()); response.headers.set("X-RateLimit-Reset", reset.toString()); return response; } export const config = { matcher: "/api/:path*", }; ``` ### Express with route-specific middleware ```typescript theme={null} // middleware/ratelimit.ts import { Ratelimit } from "@unkey/ratelimit"; import type { Request, Response, NextFunction } from "express"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1m", }); interface RateLimitOptions { limit: number; duration: string; namespace?: string; identifierFn?: (req: Request) => string; } export function rateLimit(options: RateLimitOptions) { return async (req: Request, res: Response, next: NextFunction) => { const identifier = options.identifierFn?.(req) ?? req.headers["x-user-id"] as string ?? req.ip ?? "anonymous"; const namespace = options.namespace ?? "api"; const { success, remaining, reset } = await limiter.limit( `${namespace}:${identifier}`, { limit: options.limit, duration: options.duration as any, } ); res.set({ "X-RateLimit-Limit": options.limit.toString(), "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }); if (!success) { return res.status(429).json({ error: "Rate limit exceeded", retryAfter: Math.ceil((reset - Date.now()) / 1000), }); } next(); }; } // Usage in routes import express from "express"; import { rateLimit } from "./middleware/ratelimit"; const app = express(); // Expensive AI endpoint - 10 requests per minute app.post("/api/ai/generate", rateLimit({ limit: 10, duration: "1m", namespace: "ai" }), async (req, res) => { // Call your AI provider } ); // Data export - 5 per hour app.get("/api/export/:type", rateLimit({ limit: 5, duration: "1h", namespace: "export" }), async (req, res) => { // Generate export } ); // Regular endpoints - 100 per minute (default) app.use("/api", rateLimit({ limit: 100, duration: "1m" }) ); ``` ### Hono with route groups ```typescript theme={null} import { Hono } from "hono"; import { Ratelimit } from "@unkey/ratelimit"; const app = new Hono(); const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1m", }); // Middleware factory for different limits function rateLimitMiddleware(options: { limit: number; duration: string; namespace: string; }) { return async (c: any, next: any) => { const identifier = c.req.header("x-user-id") ?? "anonymous"; const { success, remaining, reset } = await limiter.limit( `${options.namespace}:${identifier}`, { limit: options.limit, duration: options.duration as any } ); c.header("X-RateLimit-Limit", options.limit.toString()); c.header("X-RateLimit-Remaining", remaining.toString()); c.header("X-RateLimit-Reset", reset.toString()); if (!success) { return c.json({ error: "Rate limit exceeded" }, 429); } await next(); }; } // AI routes - strict limits const ai = new Hono(); ai.use("*", rateLimitMiddleware({ limit: 10, duration: "1m", namespace: "ai" })); ai.post("/generate", (c) => c.json({ result: "..." })); ai.post("/embed", (c) => c.json({ result: "..." })); // Export routes - hourly limits const exports = new Hono(); exports.use("*", rateLimitMiddleware({ limit: 5, duration: "1h", namespace: "export" })); exports.get("/csv", (c) => c.json({ url: "..." })); exports.get("/json", (c) => c.json({ url: "..." })); // Mount route groups app.route("/api/ai", ai); app.route("/api/export", exports); // Default API routes app.use("/api/*", rateLimitMiddleware({ limit: 100, duration: "1m", namespace: "api" })); export default app; ``` ## Cost-based limiting For endpoints where some operations are more expensive than others, use cost-based limiting: ```typescript theme={null} app.post("/api/ai/generate", async (req, res) => { const { model, tokens } = req.body; // Different models have different costs const cost = model === "gpt-4" ? 10 : model === "gpt-3.5" ? 2 : 1; const { success } = await limiter.limit(userId, { cost }); if (!success) { return res.status(429).json({ error: "Rate limit exceeded" }); } // Process request... }); ``` With a limit of 100/minute: * 100 cheap model calls, OR * 50 gpt-3.5 calls, OR * 10 gpt-4 calls ## Best practices Different namespaces mean separate counters. A user can hit their AI limit without affecting their regular API quota. When matching paths, put more specific patterns first. `/api/ai/*` should come before `/api/*`. Tight limits on expensive endpoints are fine, but communicate them clearly in your API docs. Use Unkey analytics to see which endpoints hit limits most often, then adjust accordingly. ## Next steps Combine with user tiers for complete rate limiting Understand how rate limiting works under the hood # Express Middleware Source: https://unkey.com/docs/cookbook/express-middleware Reusable API key authentication middleware for Express A production-ready middleware pattern for Express applications. ## Install ```bash theme={null} npm install @unkey/api express ``` ## Basic Middleware ```typescript middleware/auth.ts theme={null} import { Unkey } from "@unkey/api"; import { Request, Response, NextFunction } from "express"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); // Extend Express Request type declare global { namespace Express { interface Request { unkey?: V2KeysVerifyKeyResponseData; } } } export async function unkeyAuth( req: Request, res: Response, next: NextFunction ) { // 1. Extract API key const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing API key" }); } const apiKey = authHeader.slice(7); // 2. Verify with Unkey const { meta, data, error } = await unkey.keys.verifyKey({ key: apiKey, }); // 3. Handle errors if (error) { console.error("Unkey error:", error); return res.status(503).json({ error: "Authentication service unavailable" }); } // 4. Check validity if (!data.valid) { const status = data.code === "RATE_LIMITED" ? 429 : 401; return res.status(status).json({ error: data.code }); } // 5. Attach to request and continue req.unkey = data; next(); } ``` ## Use the Middleware ```typescript app.ts theme={null} import express from "express"; import { unkeyAuth } from "./middleware/auth"; const app = express(); // Public routes app.get("/health", (req, res) => { res.json({ status: "ok" }); }); // Protected routes app.get("/api/data", unkeyAuth, (req, res) => { // Access verification result const { identity, meta, remaining } = req.unkey!; res.json({ message: "Access granted", user: identity?.externalId, plan: meta?.plan, creditsRemaining: remaining, }); }); // Protect entire router const apiRouter = express.Router(); apiRouter.use(unkeyAuth); apiRouter.get("/users", (req, res) => { res.json({ users: [] }); }); apiRouter.post("/users", (req, res) => { res.json({ created: true }); }); app.use("/api/v1", apiRouter); app.listen(3000); ``` *** ## With Rate Limit Headers Include rate limit info in response headers: ```typescript middleware/auth.ts theme={null} export async function unkeyAuth( req: Request, res: Response, next: NextFunction ) { const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing API key" }); } const { meta, data, error } = await unkey.keys.verifyKey({ key: authHeader.slice(7), }); if (error) { return res.status(503).json({ error: "Service unavailable" }); } // Add rate limit headers if available (v2 uses ratelimits array) if (data.ratelimits?.[0]) { const rl = data.ratelimits[0]; res.set({ "X-RateLimit-Limit": rl.limit.toString(), "X-RateLimit-Remaining": rl.remaining.toString(), "X-RateLimit-Reset": rl.reset.toString(), }); } // Add remaining credits header if available if (data.credits !== undefined) { res.set("X-Credits-Remaining", data.credits.toString()); } if (!data.valid) { const status = data.code === "RATE_LIMITED" ? 429 : 401; return res.status(status).json({ error: data.code }); } req.unkey = data; next(); } ``` *** ## Permission-Based Access Create middleware that requires specific permissions: ```typescript middleware/permissions.ts theme={null} import { Unkey } from "@unkey/api"; import { Request, Response, NextFunction } from "express"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export function requirePermission(permission: string) { return async (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing API key" }); } const { meta, data, error } = await unkey.keys.verifyKey({ key: authHeader.slice(7), permissions: permission, // Check for this permission }); if (error) { return res.status(503).json({ error: "Service unavailable" }); } if (!data.valid) { if (data.code === "INSUFFICIENT_PERMISSIONS") { return res.status(403).json({ error: "Forbidden", required: permission, }); } return res.status(401).json({ error: data.code }); } req.unkey = data; next(); }; } ``` Use it: ```typescript theme={null} // Anyone with a valid key app.get("/api/data", unkeyAuth, handler); // Only keys with "admin" permission app.delete("/api/users/:id", requirePermission("admin"), deleteUser); // Only keys with "billing.read" permission app.get("/api/invoices", requirePermission("billing.read"), getInvoices); ``` *** ## Configurable Middleware Factory Create a flexible middleware that can be configured per-route: ```typescript middleware/auth.ts theme={null} import { Unkey } from "@unkey/api"; import { Request, Response, NextFunction } from "express"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); interface AuthOptions { permissions?: string; getKey?: (req: Request) => string | null; onError?: (req: Request, res: Response, error: Error) => void; onInvalid?: (req: Request, res: Response, result: V2KeysVerifyKeyResponseData) => void; } export function createAuthMiddleware(options: AuthOptions = {}) { const { permissions, getKey = (req) => req.headers.authorization?.slice(7) ?? null, onError = (req, res) => res.status(503).json({ error: "Service unavailable" }), onInvalid = (req, res, result) => res.status(401).json({ error: result.code }), } = options; return async (req: Request, res: Response, next: NextFunction) => { const apiKey = getKey(req); if (!apiKey) { return res.status(401).json({ error: "Missing API key" }); } const { meta, data, error } = await unkey.keys.verifyKey({ key: apiKey, permissions, }); if (error) { return onError(req, res, error); } if (!data.valid) { return onInvalid(req, res, data); } req.unkey = data; next(); }; } // Pre-configured middlewares export const unkeyAuth = createAuthMiddleware(); export const adminAuth = createAuthMiddleware({ permissions: "admin", onInvalid: (req, res, result) => { if (result.code === "INSUFFICIENT_PERMISSIONS") { return res.status(403).json({ error: "Admin access required" }); } return res.status(401).json({ error: result.code }); }, }); ``` *** ## Error Handling Graceful degradation when Unkey is unavailable: ```typescript middleware/auth.ts theme={null} export async function unkeyAuth( req: Request, res: Response, next: NextFunction ) { const apiKey = req.headers.authorization?.slice(7); if (!apiKey) { return res.status(401).json({ error: "Missing API key" }); } try { const { meta, data, error } = await unkey.keys.verifyKey({ key: apiKey, }); if (error) { // Log for monitoring console.error("Unkey verification error:", error); // Option 1: Fail closed (more secure) return res.status(503).json({ error: "Authentication unavailable" }); // Option 2: Fail open (better availability, less secure) // console.warn("Allowing request due to Unkey unavailability"); // return next(); } if (!data.valid) { return res.status(401).json({ error: data.code }); } req.unkey = data; next(); } catch (e) { console.error("Unexpected auth error:", e); return res.status(503).json({ error: "Authentication service error" }); } } ``` *** ## TypeScript Setup ```typescript types/express.d.ts theme={null} import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components"; declare global { namespace Express { interface Request { unkey?: V2KeysVerifyKeyResponseData; } } } export {}; ``` Make sure to include this in your `tsconfig.json`: ```json theme={null} { "compilerOptions": { "typeRoots": ["./node_modules/@types", "./types"] } } ``` # FastAPI Authentication Source: https://unkey.com/docs/cookbook/fastapi-auth Dependency injection pattern for API key auth in FastAPI A clean, reusable pattern using FastAPI's dependency injection system. ## Install ```bash theme={null} pip install unkey.py fastapi uvicorn ``` ## Basic Dependency ```python dependencies/auth.py theme={null} from fastapi import HTTPException, Security from fastapi.security import APIKeyHeader from unkey.py import Unkey import os # Configure API key header api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) # Initialize Unkey client unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) async def verify_api_key(api_key: str = Security(api_key_header)): """Dependency that verifies API keys and returns the verification result.""" if not api_key: raise HTTPException( status_code=401, detail="Missing API key", headers={"WWW-Authenticate": "ApiKey"}, ) try: result = await unkey.keys.verify_key_async( key=api_key, ) except Exception as e: raise HTTPException( status_code=503, detail="Authentication service unavailable" ) if not result.valid: status = 429 if result.code == "RATE_LIMITED" else 401 raise HTTPException(status_code=status, detail=result.code) return result ``` ## Use in Routes ```python main.py theme={null} from fastapi import FastAPI, Depends from dependencies.auth import verify_api_key app = FastAPI() @app.get("/api/data") async def get_data(auth = Depends(verify_api_key)): return { "message": "Access granted", "user": auth.identity.external_id if auth.identity else None, "remaining_credits": auth.credits, "metadata": auth.meta, } @app.get("/api/users") async def get_users(auth = Depends(verify_api_key)): # Use auth.identity.external_id to scope data access return {"users": []} ``` *** ## Typed Response Model Create a Pydantic model for the auth result: ```python models/auth.py theme={null} from pydantic import BaseModel, ConfigDict from typing import Optional, Any class Identity(BaseModel): id: str external_id: str meta: Optional[dict[str, Any]] = None class AuthResult(BaseModel): valid: bool code: str key_id: str name: Optional[str] = None identity: Optional[Identity] = None meta: Optional[dict[str, Any]] = None credits: Optional[int] = None expires: Optional[int] = None enabled: bool = True permissions: Optional[list[str]] = None roles: Optional[list[str]] = None model_config = ConfigDict(extra="allow") # Allow additional fields from Unkey ``` ```python dependencies/auth.py theme={null} from models.auth import AuthResult async def verify_api_key(api_key: str = Security(api_key_header)) -> AuthResult: if not api_key: raise HTTPException(status_code=401, detail="Missing API key") try: result = await unkey.keys.verify_key_async(key=api_key) except Exception as e: raise HTTPException(status_code=503, detail="Unkey service unavailable") if not result.valid: raise HTTPException(status_code=401, detail=result.code) return AuthResult( valid=result.valid, code=result.code, key_id=result.key_id, identity=result.identity, meta=result.meta, credits=result.credits, ) ``` *** ## Permission-Based Access Create dependencies for different permission levels: ```python dependencies/auth.py theme={null} from fastapi import HTTPException, Security from fastapi.security import APIKeyHeader from unkey.py import Unkey import os api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) def require_permission(permission: str): """Factory that creates a dependency requiring a specific permission.""" async def verify(api_key: str = Security(api_key_header)): if not api_key: raise HTTPException(status_code=401, detail="Missing API key") # Include permission check in verification result = await unkey.keys.verify_key_async( key=api_key, permissions=permission, ) if not result.valid: if result.code == "INSUFFICIENT_PERMISSIONS": raise HTTPException( status_code=403, detail=f"Permission required: {permission}" ) raise HTTPException(status_code=401, detail=result.code) return result return verify # Pre-built permission checkers require_read = require_permission("data.read") require_write = require_permission("data.write") require_admin = require_permission("admin") ``` Use in routes: ```python main.py theme={null} from dependencies.auth import require_read, require_write, require_admin @app.get("/api/data") async def read_data(auth = Depends(require_read)): return {"data": []} @app.post("/api/data") async def create_data(auth = Depends(require_write)): return {"created": True} @app.delete("/api/users/{user_id}") async def delete_user(user_id: str, auth = Depends(require_admin)): return {"deleted": user_id} ``` *** ## Rate Limit Headers Return rate limit info in response headers: ```python dependencies/auth.py theme={null} from fastapi import Response async def verify_api_key( response: Response, api_key: str = Security(api_key_header) ): if not api_key: raise HTTPException(status_code=401, detail="Missing API key") result = await unkey.keys.verify_key_async( key=api_key, ) # Add rate limit headers if result.ratelimits: rl = result.ratelimits[0] response.headers["X-RateLimit-Limit"] = str(rl.limit) response.headers["X-RateLimit-Remaining"] = str(rl.remaining) response.headers["X-RateLimit-Reset"] = str(rl.reset) # Add credits header if result.credits is not None: response.headers["X-Credits-Remaining"] = str(result.credits) if not result.valid: raise HTTPException( status_code=429 if result.code == "RATE_LIMITED" else 401, detail=result.code ) return result ``` *** ## Async Client For better performance with async FastAPI: ```python dependencies/auth.py theme={null} from contextlib import asynccontextmanager from unkey.py import Unkey import os # Global client (initialized on startup) unkey_client: Unkey = None @asynccontextmanager async def lifespan(app): global unkey_client unkey_client = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) yield # Cleanup if needed async def verify_api_key(api_key: str = Security(api_key_header)): if not api_key: raise HTTPException(status_code=401, detail="Missing API key") # Use async method result = await unkey_client.keys.verify_key_async( key=api_key, ) if not result.valid: raise HTTPException(status_code=401, detail=result.code) return result ``` ```python main.py theme={null} from fastapi import FastAPI from dependencies.auth import lifespan app = FastAPI(lifespan=lifespan) ``` *** ## Full Example ```python main.py theme={null} from fastapi import FastAPI, Depends, HTTPException, Security, Response from fastapi.security import APIKeyHeader from pydantic import BaseModel from unkey.py import Unkey import os app = FastAPI(title="My API") # Auth setup api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) # Models class DataResponse(BaseModel): message: str user: str | None remaining_credits: int | None # Dependencies async def get_auth( response: Response, api_key: str = Security(api_key_header) ): if not api_key: raise HTTPException(status_code=401, detail="Missing API key") result = await unkey.keys.verify_key_async(key=api_key) if result.credits is not None: response.headers["X-Credits-Remaining"] = str(result.credits) if not result.valid: raise HTTPException(status_code=401, detail=result.code) return result # Routes @app.get("/api/data", response_model=DataResponse) async def get_data(auth = Depends(get_auth)): return DataResponse( message="Access granted", user=auth.identity.external_id if auth.identity else None, remaining_credits=auth.credits, ) @app.get("/health") async def health(): return {"status": "ok"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) ``` Run with: ```bash theme={null} UNKEY_ROOT_KEY=... uvicorn main:app --reload ``` # Echo Framework Middleware Source: https://unkey.com/docs/cookbook/go-echo-middleware Production-ready API key middleware for Echo This recipe shows how to create robust API key authentication middleware for the [Echo web framework](https://echo.labstack.com/). ## Complete Middleware Implementation ```go theme={null} package main import ( "net/http" "os" "slices" "strings" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // UnkeyAuthMiddleware creates an Echo middleware for API key verification func UnkeyAuthMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { authHeader := c.Request().Header.Get("Authorization") if authHeader == "" { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Missing Authorization header", "code": "MISSING_KEY", }) } apiKey := strings.TrimPrefix(authHeader, "Bearer ") // Verify with Unkey res, err := unkeyClient.Keys.VerifyKey(c.Request().Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { return c.JSON(http.StatusServiceUnavailable, map[string]string{ "error": "Verification service unavailable", "code": "SERVICE_ERROR", "message": err.Error(), }) } if !res.V2KeysVerifyKeyResponseBody.Data.Valid { code := string(res.V2KeysVerifyKeyResponseBody.Data.Code) return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Invalid API key", "code": code, }) } // Store verification result in context c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data) return next(c) } } } // RequirePermission creates middleware to check specific permissions func RequirePermission(permission string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { result := c.Get("unkey") if result == nil { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) } keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok || keyResult == nil { return c.JSON(http.StatusInternalServerError, map[string]any{ "error": "Invalid authentication context", "code": "INTERNAL_ERROR", }) } if slices.Contains(keyResult.Permissions, permission) { return next(c) } return c.JSON(http.StatusForbidden, map[string]any{ "error": "Insufficient permissions", "code": "FORBIDDEN", "required": permission, }) } } } // RequireRole creates middleware to check specific roles func RequireRole(role string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { result := c.Get("unkey") if result == nil { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) } keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok || keyResult == nil { return c.JSON(http.StatusInternalServerError, map[string]any{ "error": "Invalid authentication context", "code": "INTERNAL_ERROR", }) } if slices.Contains(keyResult.Roles, role) { return next(c) } return c.JSON(http.StatusForbidden, map[string]any{ "error": "Insufficient role", "code": "FORBIDDEN", "required": role, }) } } } // GetUnkeyResult retrieves the Unkey verification result from context func GetUnkeyResult(c echo.Context) *components.V2KeysVerifyKeyResponseData { result := c.Get("unkey") if result == nil { return nil } r, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok { return nil } return r } // Usage example func main() { e := echo.New() // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Public routes e.GET("/health", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) }) // Protected routes api := e.Group("/api") api.Use(UnkeyAuthMiddleware()) { api.GET("/data", func(c echo.Context) error { result := GetUnkeyResult(c) ownerID := "" if result.Identity != nil { ownerID = result.Identity.ExternalID } return c.JSON(http.StatusOK, map[string]any{ "message": "Access granted", "key_id": *result.KeyID, "owner": ownerID, "meta": result.Meta, }) }) api.GET("/profile", func(c echo.Context) error { result := GetUnkeyResult(c) keyID := "" if result.KeyID != nil { keyID = *result.KeyID } return c.JSON(http.StatusOK, map[string]any{ "key_id": keyID, "permissions": result.Permissions, "roles": result.Roles, }) }) } // Admin routes admin := e.Group("/api/admin") admin.Use(UnkeyAuthMiddleware(), RequirePermission("admin:read")) { admin.GET("/users", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]any{ "message": "Admin access granted", "users": []string{"user1", "user2"}, }) }) admin.POST("/config", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{ "message": "Config updated", }) }, RequirePermission("admin:write")) } e.Start(":8080") } ``` ## Custom Configuration Create configurable middleware: ````go theme={null} type AuthConfig struct { HeaderName string Prefix string Optional bool } func UnkeyAuthWithConfig(config AuthConfig) echo.MiddlewareFunc { if config.HeaderName == "" { config.HeaderName = "Authorization" } if config.Prefix == "" { config.Prefix = "Bearer " } return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { authHeader := c.Request().Header.Get(config.HeaderName) if authHeader == "" { if config.Optional { return next(c) } return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Missing API key", "code": "MISSING_KEY", }) } apiKey := strings.TrimPrefix(authHeader, config.Prefix) res, err := unkeyClient.Keys.VerifyKey(c.Request().Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { return c.JSON(http.StatusServiceUnavailable, map[string]string{ "error": "Verification failed", }) } if !res.V2KeysVerifyKeyResponseBody.Data.Valid { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Invalid API key", }) } c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data) return next(c) } } } // Usage with custom config e.GET("/api/public", func(c echo.Context) error { result := GetUnkeyResult(c) if result != nil { keyID := "" if result.KeyID != nil { keyID = *result.KeyID } return c.JSON(http.StatusOK, map[string]any{ "message": "Authenticated", "key_id": keyID, }) } return c.JSON(http.StatusOK, map[string]string{ "message": "Anonymous", }) }, UnkeyAuthWithConfig(AuthConfig{Optional: true})) ## Group-Level Middleware Apply middleware to route groups: ```go // API v1 routes v1 := e.Group("/api/v1") v1.Use(UnkeyAuthMiddleware()) // Public subset public := v1.Group("/public") public.GET("/status", statusHandler) // Protected subset protected := v1.Group("/protected") protected.Use(RequirePermission("data:read")) protected.GET("/data", dataHandler) ```` ## Testing ```bash theme={null} # Test without key curl http://localhost:8080/api/data # {"error":"Missing Authorization header","code":"MISSING_KEY"} # Test with valid key curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/data # {"message":"Access granted","key_id":"key_..."} # Test admin route without permission curl -H "Authorization: Bearer USER_KEY" http://localhost:8080/api/admin/users # {"error":"Insufficient permissions","code":"FORBIDDEN"} # Test admin route with permission curl -H "Authorization: Bearer ADMIN_KEY" http://localhost:8080/api/admin/users # {"message":"Admin access granted","users":["user1","user2"]} ``` ## Related Get started with Go and Unkey Complete Go SDK documentation # Gin Framework Middleware Source: https://unkey.com/docs/cookbook/go-gin-middleware Production-ready API key middleware for Gin This recipe shows how to create robust API key authentication middleware for the [Gin web framework](https://gin-gonic.com/). ## Complete Middleware Implementation ```go theme={null} package main import ( "net/http" "os" "slices" "strconv" "strings" "github.com/gin-gonic/gin" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // UnkeyAuth creates a Gin middleware for API key verification func UnkeyAuth() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Missing Authorization header", "code": "MISSING_KEY", }) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") // Verify with Unkey res, err := unkeyClient.Keys.VerifyKey(c.Request.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{ "error": "Verification service unavailable", "code": "SERVICE_ERROR", "message": err.Error(), }) return } if !res.V2KeysVerifyKeyResponseBody.Data.Valid { code := string(res.V2KeysVerifyKeyResponseBody.Data.Code) c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Invalid API key", "code": code, }) return } // Store verification result in context (as pointer for type assertion compatibility) c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data) c.Next() } } // RequirePermission creates middleware to check specific permissions func RequirePermission(permission string) gin.HandlerFunc { return func(c *gin.Context) { result, exists := c.Get("unkey") if !exists { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) return } keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok || keyResult == nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ "error": "Invalid authentication context", "code": "INTERNAL_ERROR", }) return } if slices.Contains(keyResult.Permissions, permission) { c.Next() return } c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": "Insufficient permissions", "code": "FORBIDDEN", "required": permission, }) } } // RequireRole creates middleware to check specific roles func RequireRole(role string) gin.HandlerFunc { return func(c *gin.Context) { result, exists := c.Get("unkey") if !exists { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) return } keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok || keyResult == nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ "error": "Invalid authentication context", "code": "INTERNAL_ERROR", }) return } if slices.Contains(keyResult.Roles, role) { c.Next() return } c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": "Insufficient role", "code": "FORBIDDEN", "required": role, }) } } // GetUnkeyResult retrieves the Unkey verification result from context func GetUnkeyResult(c *gin.Context) *components.V2KeysVerifyKeyResponseData { result, ok := c.Get("unkey") if !ok { return nil } r, ok := result.(*components.V2KeysVerifyKeyResponseData) if !ok { return nil } return r } // Usage example func main() { r := gin.Default() // Public routes r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) // Protected API group api := r.Group("/api") api.Use(UnkeyAuth()) { api.GET("/data", func(c *gin.Context) { result := GetUnkeyResult(c) ownerID := "" if result.Identity != nil { ownerID = result.Identity.ExternalID } c.JSON(http.StatusOK, gin.H{ "message": "Access granted", "key_id": *result.KeyID, "owner": ownerID, "meta": result.Meta, }) }) api.GET("/profile", func(c *gin.Context) { result := GetUnkeyResult(c) c.JSON(http.StatusOK, gin.H{ "key_id": *result.KeyID, "permissions": result.Permissions, "roles": result.Roles, }) }) } // Admin routes with permission check admin := r.Group("/api/admin") admin.Use(UnkeyAuth(), RequirePermission("admin:read")) { admin.GET("/users", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "Admin access granted", "users": []string{"user1", "user2"}, }) }) admin.POST("/config", RequirePermission("admin:write"), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "Config updated", }) }) } r.Run(":8080") } ``` ## Rate Limiting Integration Combine with Unkey's rate limiting: ```go theme={null} func RateLimitMiddleware(namespace string) gin.HandlerFunc { return func(c *gin.Context) { result := GetUnkeyResult(c) if result == nil { c.Next() return } // Use the key's built-in rate limits from verification // These are already checked during key verification // Or use standalone rate limit API for custom limits res, err := unkeyClient.Ratelimits.Limit(c.Request.Context(), components.V2RatelimitsLimitRequestBody{ Namespace: namespace, Identifier: *result.KeyID, Limit: 100, Duration: 60000, // 100 per minute }) if err != nil { c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{ "error": "Rate limit check failed", }) return } if !res.V2RatelimitsLimitResponseBody.Success { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ "error": "Rate limit exceeded", "reset": res.V2RatelimitsLimitResponseBody.Reset, "limit": 100, "window": "60s", }) return } // Add rate limit headers c.Header("X-RateLimit-Limit", "100") c.Header("X-RateLimit-Remaining", strconv.FormatInt(res.V2RatelimitsLimitResponseBody.Remaining, 10)) c.Next() } } ``` ## Testing ```bash theme={null} # Test without key curl http://localhost:8080/api/data # {"error":"Missing Authorization header","code":"MISSING_KEY"} # Test with valid key curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/data # {"message":"Access granted","key_id":"key_..."} # Test admin route without permission curl -H "Authorization: Bearer USER_KEY" http://localhost:8080/api/admin/users # {"error":"Insufficient permissions","code":"FORBIDDEN"} # Test admin route with permission curl -H "Authorization: Bearer ADMIN_KEY" http://localhost:8080/api/admin/users # {"message":"Admin access granted","users":["user1","user2"]} ``` ## Related Get started with Go and Unkey Complete Go SDK documentation # Go Standard Library Middleware Source: https://unkey.com/docs/cookbook/go-stdlib-middleware Production-ready API key middleware for Go's net/http This recipe shows how to create robust API key authentication middleware for Go's standard library HTTP server. ## Complete Middleware Implementation ```go theme={null} package main import ( "context" "encoding/json" "net/http" "os" "slices" "strings" "time" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) // KeyContext stores Unkey verification result in request context type KeyContext struct { KeyID string OwnerID string Meta map[string]any Permissions []string Roles []string } // contextKey is the key type for storing Unkey context type contextKey string const unkeyContextKey contextKey = "unkey" var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // AuthMiddleware creates a middleware that verifies API keys func AuthMiddleware(opts ...AuthOption) func(http.Handler) http.Handler { options := &authOptions{ headerName: "Authorization", prefix: "Bearer ", required: true, } for _, opt := range opts { opt(options) } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") // Extract API key authHeader := r.Header.Get(options.headerName) if authHeader == "" { if options.required { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": "Missing API key", "code": "MISSING_KEY", }) return } // Auth not required, continue without verification next.ServeHTTP(w, r) return } apiKey := strings.TrimPrefix(authHeader, options.prefix) // Verify with Unkey ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() res, err := unkeyClient.Keys.VerifyKey(ctx, components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]string{ "error": "Verification service unavailable", "code": "SERVICE_ERROR", "message": err.Error(), }) return } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { code := string(result.Code) w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]any{ "error": "Invalid API key", "code": code, }) return } // Build context keyCtx := &KeyContext{ KeyID: *result.KeyID, Meta: result.Meta, Permissions: result.Permissions, Roles: result.Roles, } if result.Identity != nil { keyCtx.OwnerID = result.Identity.ExternalID } // Store in request context ctx = context.WithValue(r.Context(), unkeyContextKey, keyCtx) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // AuthOption configures the auth middleware type authOptions struct { headerName string prefix string required bool } type AuthOption func(*authOptions) // WithHeaderName sets a custom header name for the API key func WithHeaderName(name string) AuthOption { return func(o *authOptions) { o.headerName = name } } // WithPrefix sets a custom prefix for the API key func WithPrefix(prefix string) AuthOption { return func(o *authOptions) { o.prefix = prefix } } // WithOptional makes authentication optional func WithOptional() AuthOption { return func(o *authOptions) { o.required = false } } // GetKeyContext retrieves the Unkey context from request func GetKeyContext(r *http.Request) (*KeyContext, bool) { ctx, ok := r.Context().Value(unkeyContextKey).(*KeyContext) return ctx, ok } // RequirePermission middleware checks if the key has a specific permission func RequirePermission(permission string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { keyCtx, ok := GetKeyContext(r) if !ok { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{ "error": "Authentication required", "code": "AUTH_REQUIRED", }) return } if slices.Contains(keyCtx.Permissions, permission) { next.ServeHTTP(w, r) return } w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{ "error": "Insufficient permissions", "code": "FORBIDDEN", "required": permission, }) }) } } // Usage example func main() { mux := http.NewServeMux() // Public route mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) }) // Protected routes mux.Handle("/api/protected", AuthMiddleware()(http.HandlerFunc(protectedHandler))) // Protected with permission check // Compose middleware: AuthMiddleware wraps RequirePermission which wraps the handler mux.Handle("/api/admin", AuthMiddleware()(RequirePermission("admin:read")(http.HandlerFunc(adminHandler)))) // Optional auth mux.Handle("/api/public", AuthMiddleware(WithOptional())(http.HandlerFunc(optionalAuthHandler))) server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, } server.ListenAndServe() } func protectedHandler(w http.ResponseWriter, r *http.Request) { keyCtx, _ := GetKeyContext(r) json.NewEncoder(w).Encode(map[string]any{ "message": "Access granted", "key_id": keyCtx.KeyID, "owner": keyCtx.OwnerID, }) } func adminHandler(w http.ResponseWriter, r *http.Request) { keyCtx, _ := GetKeyContext(r) json.NewEncoder(w).Encode(map[string]any{ "message": "Admin access granted", "key_id": keyCtx.KeyID, }) } func optionalAuthHandler(w http.ResponseWriter, r *http.Request) { keyCtx, ok := GetKeyContext(r) if ok { json.NewEncoder(w).Encode(map[string]any{ "message": "Authenticated access", "key_id": keyCtx.KeyID, }) } else { json.NewEncoder(w).Encode(map[string]any{ "message": "Anonymous access", }) } } ``` ## Key Features * **Context propagation** - Key info stored in request context * **Permission checking** - Middleware to check specific permissions * **Optional auth** - Support for optional authentication * **Custom headers** - Configurable header names and prefixes * **Timeout handling** - Request timeouts for Unkey API calls * **Error responses** - Structured JSON error responses ## Testing ```bash theme={null} # Start server go run main.go # Test protected route without key curl http://localhost:8080/api/protected # {"error":"Missing API key","code":"MISSING_KEY"} # Test protected route with valid key curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/protected # {"message":"Access granted","key_id":"key_..."} # Test public route (no auth required) curl http://localhost:8080/api/public # {"message":"Anonymous access"} # Test public route with auth curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/public # {"message":"Authenticated access","key_id":"key_..."} # Test admin route without permission curl -H "Authorization: Bearer USER_API_KEY" http://localhost:8080/api/admin # {"error":"Insufficient permissions","code":"FORBIDDEN","required":"admin:read"} # Test admin route with permission curl -H "Authorization: Bearer ADMIN_API_KEY" http://localhost:8080/api/admin # {"message":"Admin access granted","key_id":"key_..."} ``` ## Related Complete Go SDK documentation # Overview Source: https://unkey.com/docs/cookbook/index Copy-paste recipes for common Unkey patterns Real-world examples you can drop into your codebase. Each recipe is self-contained and production-ready. ## API Key Authentication Protect App Router and Pages Router API routes Reusable auth middleware for Express apps Dependency injection pattern for FastAPI Decorator-based auth for Flask applications Production-ready net/http middleware Middleware for Gin web framework Middleware for Echo web framework Edge-first API authentication ## Rate Limiting Different limits based on user tier Tighter limits on expensive operations Implement rate limiting in FastAPI ## Usage & Billing Track and bill based on API usage Free, Pro, Enterprise with different limits # Next.js API Routes Source: https://unkey.com/docs/cookbook/nextjs-api-routes Protect your Next.js API routes with Unkey authentication Two approaches: use the `@unkey/nextjs` wrapper for simplicity, or manual verification for full control. ## Option 1: Using @unkey/nextjs (Recommended) The simplest way to protect Next.js API routes. ### Install ```bash theme={null} npm install @unkey/nextjs ``` ### App Router ```typescript app/api/protected/route.ts theme={null} import { withUnkey, NextRequestWithUnkeyContext } from "@unkey/nextjs"; export const POST = withUnkey( async (req: NextRequestWithUnkeyContext) => { // req.unkey contains verification result const { identity } = req.unkey.data; return Response.json({ message: "Access granted", user: identity?.externalId, }); }, { rootKey: process.env.UNKEY_ROOT_KEY! } ); ``` ### Pages Router ```typescript pages/api/protected.ts theme={null} import { withUnkey, NextRequestWithUnkeyContext } from "@unkey/nextjs"; import { NextApiRequest, NextApiResponse } from "next"; async function handler(req: NextApiRequest, res: NextApiResponse) { // Type assertion needed for Pages Router const unkeyReq = req as unknown as NextRequestWithUnkeyContext; return res.json({ message: "Access granted", user: unkeyReq.unkey?.data.identity?.externalId, }); } export default withUnkey(handler, { rootKey: process.env.UNKEY_ROOT_KEY! }); ``` ### Custom key extraction By default, `withUnkey` looks for a Bearer token in the `Authorization` header. Customize this: ```typescript theme={null} export const POST = withUnkey( async (req) => { /* ... */ }, { rootKey: process.env.UNKEY_ROOT_KEY!, getKey: (req) => req.headers.get("x-api-key"), // Custom header } ); ``` ### Custom error handling ```typescript theme={null} export const POST = withUnkey( async (req) => { /* ... */ }, { rootKey: process.env.UNKEY_ROOT_KEY!, handleInvalidKey: (req, result) => { // result.code tells you why it failed return Response.json( { error: "Unauthorized", reason: result.code, // "NOT_FOUND", "EXPIRED", "RATE_LIMITED", etc. }, { status: 401 } ); }, onError: (req, error) => { console.error("Unkey error:", error); return Response.json( { error: "Authentication service unavailable" }, { status: 503 } ); }, } ); ``` *** ## Option 2: Manual Verification For full control over the authentication flow. ```typescript app/api/protected/route.ts theme={null} import { Unkey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { NextRequest, NextResponse } from "next/server"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function POST(req: NextRequest) { // 1. Extract API key const authHeader = req.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return NextResponse.json( { error: "Missing API key" }, { status: 401 } ); } const apiKey = authHeader.slice(7); // 2. Verify with Unkey try { const { meta, data } = await unkey.keys.verifyKey({ key: apiKey, }); // You can reject the request because the key is invalid. if (!data.valid) { return Response.json({ error: "Invalid API key" }, { status: 401 }); } // Perform your operations return Response.json({ data: "hello world" }); } catch (err) { // handle our errors however you want. if (err instanceof UnkeyError) { console.error("Unkey API Error:", { statusCode: err.statusCode, body: err.body, message: err.message, }); return Response.json( { error: "API error occurred", details: err.message }, { status: err.statusCode }, ); } // Handle generic errors console.log("Unknown error:", err); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } } ``` *** ## Reusable Middleware Pattern Create a helper for consistent auth across routes: ```typescript lib/auth.ts theme={null} import { Unkey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { NextRequest, NextResponse } from "next/server"; import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components"; type AuthenticatedHandler = ( req: NextRequest, auth: V2KeysVerifyKeyResponseData, ) => Promise; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export function withAuth(handler: AuthenticatedHandler) { return async (req: NextRequest) => { const authHeader = req.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return NextResponse.json({ error: "Missing API key" }, { status: 401 }); } try { const { meta, data } = await unkey.keys.verifyKey({ key: authHeader.slice(7), }); if (!data.valid) { return NextResponse.json({ error: data.code }, { status: 401 }); } return handler(req, data); } catch (error) { if (error instanceof UnkeyError) { return NextResponse.json( { error: error.message }, { status: error.statusCode }, ); } return NextResponse.json( { error: "Service unavailable" }, { status: 503 }, ); } }; } ``` Use it: ```typescript app/api/users/route.ts theme={null} import { withAuth } from "@/lib/auth"; export const GET = withAuth(async (req, auth) => { // auth contains the full verification result const users = await db.users.findMany({ where: { organizationId: auth.identity?.externalId }, }); return Response.json({ users }); }); import { withAuth } from "../../lib/auth/withauth"; import { NextResponse } from "next/server"; export const GET = withAuth(async (_req, auth) => { // auth contains the full verification result so you can look at details const users = await db.users.findMany({ where: { organizationId: auth.identity?.externalId }, }); return NextResponse.json({ users }); }); ``` *** ## Check Permissions Require specific permissions for sensitive endpoints: ```typescript app/api/admin/route.ts theme={null} import { verifyKey } from "@unkey/api"; import { UnkeyError } from "@unkey/api/models/errors"; import { NextRequest, NextResponse } from "next/server"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function POST(req: NextRequest) { // 1. Extract API key const authHeader = req.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return NextResponse.json( { error: "Missing API key" }, { status: 401 } ); } const apiKey = authHeader.slice(7); // 2. Verify with Unkey try { const result = await unkey.keys.verifyKey({ key: apiKey, permissions: ["admin.delete"], }); // You can reject the request because the key is invalid. if (!result.data.valid) { if (result.data.code === "INSUFFICIENT_PERMISSIONS") { return Response.json({ error: "Admin access required" }, { status: 403 }); } return Response.json({ error: "Unauthorized" }, { status: 401 }); } return Response.json({ data: "hello world" }); } catch (err) { // handle our errors however you want. if (err instanceof UnkeyError) { console.error("Unkey API Error:", { statusCode: err.statusCode, body: err.body, message: err.message, }); return Response.json( { error: "API error occurred", details: err.message }, { status: err.statusCode }, ); } // Handle generic errors console.log("Unknown error:", err); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } } ``` *** ## Environment Setup ```bash .env.local theme={null} # Required for verification and key management UNKEY_ROOT_KEY=unkey_... ``` Never expose `UNKEY_ROOT_KEY` to the client. It should only be used in server-side code. # Per-User Rate Limits Source: https://unkey.com/docs/cookbook/per-user-ratelimit Apply different rate limits based on user tier or plan Apply different rate limits to different users based on their subscription tier, role, or any other criteria. This recipe shows how to implement tiered rate limiting without hardcoding limits in your application. ## The pattern ```typescript theme={null} // Define limits per tier const TIER_LIMITS = { free: { limit: 100, duration: "1h" }, pro: { limit: 1000, duration: "1h" }, enterprise: { limit: 10000, duration: "1h" }, }; // Get user's tier and apply appropriate limit const tier = await getUserTier(userId); const config = TIER_LIMITS[tier]; const { success } = await limiter.limit(userId, { limit: config.limit, duration: config.duration, }); ``` ## Full implementation ### Next.js API Route ```typescript theme={null} // app/api/route.ts import { Ratelimit } from "@unkey/ratelimit"; import { headers } from "next/headers"; import { NextResponse } from "next/server"; // Initialize with default limits (will be overridden per-request) const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1h", }); const TIER_LIMITS: Record = { free: { limit: 100, duration: "1h" }, pro: { limit: 1000, duration: "1h" }, enterprise: { limit: 10000, duration: "1h" }, }; async function getUserTier(userId: string): Promise { // Replace with your actual user lookup // e.g., database query, auth provider, etc. const user = await db.users.findUnique({ where: { id: userId } }); return user?.tier ?? "free"; } export async function POST(request: Request) { const headersList = headers(); const userId = headersList.get("x-user-id"); if (!userId) { return NextResponse.json({ error: "Missing user ID" }, { status: 401 }); } // Get user's tier const tier = await getUserTier(userId); const config = TIER_LIMITS[tier] ?? TIER_LIMITS.free; // Apply tier-specific rate limit const { success, remaining, reset } = await limiter.limit(userId, { limit: config.limit, duration: config.duration as any, }); if (!success) { return NextResponse.json( { error: "Rate limit exceeded", tier, reset: new Date(reset).toISOString(), }, { status: 429, headers: { "X-RateLimit-Limit": config.limit.toString(), "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": reset.toString(), }, } ); } // Your API logic here return NextResponse.json({ message: "Success", tier, remaining, }); } ``` ### Express Middleware ```typescript theme={null} // middleware/ratelimit.ts import { Ratelimit } from "@unkey/ratelimit"; import type { Request, Response, NextFunction } from "express"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1h", }); const TIER_LIMITS: Record = { free: { limit: 100, duration: "1h" }, pro: { limit: 1000, duration: "1h" }, enterprise: { limit: 10000, duration: "1h" }, }; export function tieredRateLimit() { return async (req: Request, res: Response, next: NextFunction) => { const userId = req.headers["x-user-id"] as string; if (!userId) { return res.status(401).json({ error: "Missing user ID" }); } // Get tier from your auth system const tier = req.user?.tier ?? "free"; const config = TIER_LIMITS[tier] ?? TIER_LIMITS.free; const { success, remaining, reset } = await limiter.limit(userId, { limit: config.limit, duration: config.duration as any, }); // Always set rate limit headers res.set({ "X-RateLimit-Limit": config.limit.toString(), "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), "X-RateLimit-Tier": tier, }); if (!success) { return res.status(429).json({ error: "Rate limit exceeded", tier, retryAfter: Math.ceil((reset - Date.now()) / 1000), }); } next(); }; } ``` ## Using Unkey overrides (recommended) Instead of managing limits in your code, use [Unkey overrides](/ratelimiting/overrides) to set per-user limits dynamically: ```typescript theme={null} import { Ratelimit } from "@unkey/ratelimit"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, // Default for free tier duration: "1h", }); // When a user upgrades to Pro, set an override await limiter.setOverride({ identifier: userId, limit: 1000, duration: "1h", }); // Now this user automatically gets 1000/hour instead of 100 const { success } = await limiter.limit(userId); ``` This approach means: * No code changes when limits change * Overrides can be managed via API or dashboard * Default limit applies to users without overrides ## With API key verification If you're already using Unkey for API keys, attach rate limits directly to keys: ```typescript theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); // Create a Pro tier key with higher limits try { const { meta, data, error } = await unkey.keys.create({ apiId: "api_xxx", name: "Pro User Key", ratelimit: { limit: 1000, duration: 3600000, // 1 hour in ms }, meta: { tier: "pro", }, }); if (error) { throw error; } } catch (err) { console.error(err); throw err; } // Verification automatically enforces the key's rate limit const { meta, data } = await unkey.keys.verify({ key: userKey }); if (!data.valid) { if (data.code === "RATE_LIMITED") { // Key-specific limit exceeded } } ``` ## Best practices Always use the same identifier format (user ID, org ID) for accurate limiting across requests. Return rate limit headers so clients know their limits and can back off gracefully. Pro/Enterprise users often expect some burst capacity. Consider slightly higher limits with shorter windows. Track when users hit limits to inform pricing decisions and identify potential abuse. ## Next steps Manage per-user limits without code changes Full subscription tier implementation # Tiered Subscriptions Source: https://unkey.com/docs/cookbook/tiered-subscriptions Implement Free, Pro, and Enterprise tiers with different limits A complete pattern for SaaS subscription tiers with different API limits, rate limits, and features. ## The Pattern 1. Store the user's plan in key metadata 2. Configure limits based on plan at key creation 3. Optionally upgrade/downgrade by updating the key ## Define Your Tiers ```typescript lib/tiers.ts theme={null} export const TIERS = { free: { name: "Free", credits: 100, refill: { interval: "daily" as const, amount: 100 }, rateLimit: { limit: 10, duration: 60000 }, // 10/min features: ["api_access"], }, pro: { name: "Pro", credits: 10000, refill: { interval: "monthly" as const, amount: 10000 }, rateLimit: { limit: 100, duration: 60000 }, // 100/min features: ["api_access", "webhooks", "priority_support"], }, enterprise: { name: "Enterprise", credits: null, // Unlimited refill: null, rateLimit: { limit: 1000, duration: 60000 }, // 1000/min features: ["api_access", "webhooks", "priority_support", "sla", "custom_domain"], }, } as const; export type Tier = keyof typeof TIERS; ``` ## Create Keys for Each Tier ```typescript lib/keys.ts theme={null} import { Unkey } from "@unkey/api"; import { TIERS, Tier } from "./tiers"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function createKeyForUser(userId: string, tier: Tier) { const config = TIERS[tier]; const { meta, data, error } = await unkey.keys.create({ apiId: process.env.UNKEY_API_ID!, prefix: `sk_${tier}`, externalId: userId, name: `${config.name} API Key`, // Set credits based on tier (skip for unlimited) ...(config.credits && { remaining: config.credits, refill: config.refill ?? undefined, }), // Set rate limits ratelimits: [{ name: "requests", limit: config.rateLimit.limit, duration: config.rateLimit.duration, }], // Store plan info in metadata meta: { tier, plan: config.name, features: config.features, }, }); if (error) throw error; return data; } ``` ## Check Features During Verification ```typescript middleware/auth.ts theme={null} import { verifyKey } from "@unkey/api"; export async function verifyAndCheckFeature( apiKey: string, requiredFeature?: string ) { const { meta, data, error } = await verifyKey({ key: apiKey, apiId: process.env.UNKEY_API_ID!, }); if (error) throw error; if (!data.valid) { return { valid: false, code: data.code }; } // Check feature access if (requiredFeature) { const features = (data.meta?.features as string[]) ?? []; if (!features.includes(requiredFeature)) { return { valid: false, code: "FEATURE_NOT_AVAILABLE", tier: data.meta?.tier, }; } } return { valid: true, data }; } ``` ## Upgrade/Downgrade Keys ```typescript lib/keys.ts theme={null} export async function changeUserTier(keyId: string, newTier: Tier) { const config = TIERS[newTier]; await unkey.keys.update({ keyId, // Update credits ...(config.credits ? { credits: { remaining: config.credits, refill: config.refill ?? undefined, }, } : { // Remove credits for unlimited tier remaining: null, refill: null, }), // Update rate limits ratelimits: [{ name: "requests", limit: config.rateLimit.limit, duration: config.rateLimit.duration, }], // Update metadata meta: { tier: newTier, plan: config.name, features: config.features, }, }); } ``` *** ## Express Example ```typescript app.ts theme={null} import express from "express"; import { verifyKey } from "@unkey/api"; import { TIERS } from "./lib/tiers"; const app = express(); // Middleware that checks tier and feature access function requireFeature(feature: string) { return async (req, res, next) => { const apiKey = req.headers.authorization?.slice(7); if (!apiKey) { return res.status(401).json({ error: "Missing API key" }); } const { meta, data, error } = await verifyKey({ key: apiKey, apiId: process.env.UNKEY_API_ID!, }); if (error || !data.valid) { return res.status(401).json({ error: data?.code ?? "Unauthorized" }); } const features = (data.meta?.features as string[]) ?? []; if (!features.includes(feature)) { return res.status(403).json({ error: "Feature not available on your plan", required: feature, currentTier: data.meta?.tier, upgrade: "https://yourapp.com/upgrade", }); } req.user = { id: data.identity?.externalId, tier: data.meta?.tier as string, features, }; next(); }; } // Basic API access (all tiers) app.get("/api/data", requireFeature("api_access"), (req, res) => { res.json({ data: [] }); }); // Webhooks (Pro and Enterprise only) app.post("/api/webhooks", requireFeature("webhooks"), (req, res) => { res.json({ webhook: "created" }); }); // SLA endpoint (Enterprise only) app.get("/api/sla-status", requireFeature("sla"), (req, res) => { res.json({ sla: "99.99%" }); }); app.listen(3000); ``` *** ## Next.js Example ```typescript app/api/webhooks/route.ts theme={null} import { verifyKey } from "@unkey/api"; import { NextRequest, NextResponse } from "next/server"; export async function POST(req: NextRequest) { const apiKey = req.headers.get("authorization")?.slice(7); if (!apiKey) { return NextResponse.json({ error: "Missing API key" }, { status: 401 }); } const { meta, data, error } = await verifyKey({ key: apiKey, apiId: process.env.UNKEY_API_ID!, }); if (error || !data.valid) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const features = (data.meta?.features as string[]) ?? []; if (!features.includes("webhooks")) { return NextResponse.json({ error: "Webhooks require a Pro or Enterprise plan", currentTier: data.meta?.tier, upgradeUrl: "/settings/billing", }, { status: 403 }); } // Process webhook creation... return NextResponse.json({ created: true }); } ``` *** ## Handling Plan Changes When a user upgrades via Stripe/billing: ```typescript webhooks/stripe.ts theme={null} import Stripe from "stripe"; import { changeUserTier } from "@/lib/keys"; export async function handleSubscriptionChange(event: Stripe.Event) { const subscription = event.data.object as Stripe.Subscription; // Map Stripe price IDs to tiers const priceToTier: Record = { "price_free": "free", "price_pro": "pro", "price_enterprise": "enterprise", }; const newTier = priceToTier[subscription.items.data[0].price.id]; const userId = subscription.metadata.userId; // Get user's key ID from your database const keyId = await db.users.getKeyId(userId); // Update the key await changeUserTier(keyId, newTier); // Optionally notify the user await sendEmail(userId, `You've been upgraded to ${newTier}!`); } ``` *** ## Dashboard Display Show users their current usage: ```typescript app/api/usage/route.ts theme={null} import { verifyKey } from "@unkey/api"; export async function GET(req: NextRequest) { const apiKey = req.headers.get("authorization")?.slice(7)!; const { meta, data } = await verifyKey({ key: apiKey, apiId: process.env.UNKEY_API_ID!, }); if (!data?.valid) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const tier = TIERS[data.meta?.tier as Tier]; return NextResponse.json({ tier: data.meta?.tier, plan: tier.name, usage: { credits: { used: tier.credits ? tier.credits - (data.remaining ?? 0) : null, limit: tier.credits, remaining: data.remaining, unlimited: tier.credits === null, }, rateLimit: { limit: tier.rateLimit.limit, window: `${tier.rateLimit.duration / 1000}s`, }, }, features: tier.features, }); } ``` Response: ```json theme={null} { "tier": "pro", "plan": "Pro", "usage": { "credits": { "used": 1234, "limit": 10000, "remaining": 8766, "unlimited": false }, "rateLimit": { "limit": 100, "window": "60s" } }, "features": ["api_access", "webhooks", "priority_support"] } ``` # Usage-Based Billing Source: https://unkey.com/docs/cookbook/usage-billing Track API usage per customer for metered billing Charge customers based on how much they use your API. This recipe shows how to track usage with Unkey's credits system and integrate with billing providers like Stripe. ## The pattern ```typescript theme={null} // Create a key with monthly credits const key = await unkey.keys.create({ apiId: "api_xxx", remaining: 10000, // 10,000 API calls included refill: { interval: "monthly", amount: 10000, }, }); // Each verification decrements remaining const { meta, data } = await unkey.keys.verify({ key: userKey }); if (!data.valid && data.code === "USAGE_EXCEEDED") { // Prompt user to upgrade or pay for overage } // Check remaining credits console.log(data.credits); // 9,999 after first call ``` ## Full implementation ### Setting up usage tracking ```typescript theme={null} // lib/unkey.ts import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); interface CreateBillingKeyOptions { customerId: string; plan: "starter" | "growth" | "scale"; stripeCustomerId?: string; } const PLAN_CREDITS = { starter: 1_000, growth: 10_000, scale: 100_000, }; export async function createBillingKey(options: CreateBillingKeyOptions) { const credits = PLAN_CREDITS[options.plan]; const { meta, data, error } = await unkey.keys.create({ apiId: process.env.UNKEY_API_ID!, externalId: options.customerId, // Link to your user/org name: `${options.plan} plan`, remaining: credits, refill: { interval: "monthly", amount: credits, }, meta: { plan: options.plan, stripeCustomerId: options.stripeCustomerId, createdAt: new Date().toISOString(), }, }); if (error) throw error; return data; } ``` ### API route with usage tracking ```typescript theme={null} // app/api/route.ts import { Unkey } from "@unkey/api"; import { NextResponse } from "next/server"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function POST(request: Request) { const apiKey = request.headers.get("authorization")?.replace("Bearer ", ""); if (!apiKey) { return NextResponse.json({ error: "Missing API key" }, { status: 401 }); } const { meta, data, error } = await unkey.keys.verifyKey({ key: apiKey, }); if (error) { return NextResponse.json({ error: "Verification failed" }, { status: 500 }); } if (!data.valid) { if (data.code === "USAGE_EXCEEDED") { return NextResponse.json( { error: "Usage limit exceeded", remaining: 0, message: "Please upgrade your plan or wait for monthly reset", }, { status: 402 } // Payment Required ); } return NextResponse.json( { error: "Invalid API key", code: data.code }, { status: 401 } ); } // Include usage info in response headers const response = NextResponse.json({ success: true, // Your API response... }); response.headers.set("X-Usage-Remaining", (data.credits ?? 0).toString()); return response; } ``` ### Stripe integration for overages ```typescript theme={null} // lib/billing.ts import Stripe from "stripe"; import { Unkey } from "@unkey/api"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); // Record overage usage in Stripe export async function recordOverage(keyId: string, amount: number) { // Get the key to find the Stripe customer const { data: key } = await unkey.keys.get({ keyId }); if (!key?.meta?.stripeCustomerId) { throw new Error("No Stripe customer linked to this key"); } // Find or create a usage-based subscription item const subscriptions = await stripe.subscriptions.list({ customer: key.meta.stripeCustomerId as string, status: "active", }); const subscription = subscriptions.data[0]; if (!subscription) { throw new Error("No active subscription"); } // Find the metered price item const meteredItem = subscription.items.data.find( (item) => item.price.recurring?.usage_type === "metered" ); if (meteredItem) { // Report usage to Stripe await stripe.subscriptionItems.createUsageRecord(meteredItem.id, { quantity: amount, timestamp: Math.floor(Date.now() / 1000), action: "increment", }); } } // Webhook handler for when credits run out export async function handleUsageExceeded(keyId: string) { const { data: key } = await unkey.keys.get({ keyId }); if (key?.meta?.stripeCustomerId) { // Option 1: Add overage credits and bill later await unkey.keys.update({ keyId, remaining: 1000, // Grant overage allowance }); await recordOverage(keyId, 1000); // Option 2: Or just notify and let them upgrade // await sendUpgradeEmail(key.meta.email); } } ``` ### Usage dashboard endpoint ```typescript theme={null} // app/api/usage/route.ts import { Unkey } from "@unkey/api"; import { NextResponse } from "next/server"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); export async function GET(request: Request) { const customerId = request.headers.get("x-customer-id"); if (!customerId) { return NextResponse.json({ error: "Missing customer ID" }, { status: 401 }); } // Get all keys for this customer const { data } = await unkey.apis.listKeys({ apiId: process.env.UNKEY_API_ID!, externalId: customerId, }); if (!data?.keys.length) { return NextResponse.json({ error: "No keys found" }, { status: 404 }); } const key = data.keys[0]; return NextResponse.json({ plan: key.meta?.plan ?? "starter", usage: { remaining: key.remaining ?? 0, limit: key.refill?.amount ?? 0, used: (key.refill?.amount ?? 0) - (key.remaining ?? 0), resetsAt: key.refill?.lastRefillAt ? new Date(new Date(key.refill.lastRefillAt).getTime() + 30 * 24 * 60 * 60 * 1000) : null, }, }); } ``` ## Plan upgrades When a customer upgrades, update their key: ```typescript theme={null} export async function upgradePlan( keyId: string, newPlan: "starter" | "growth" | "scale" ) { const newCredits = PLAN_CREDITS[newPlan]; // Get current usage const { data: currentKey } = await unkey.keys.get({ keyId }); const currentRemaining = currentKey?.remaining ?? 0; // Option 1: Add the difference (pro-rated) const creditsToAdd = newCredits - (currentKey?.refill?.amount ?? 0); await unkey.keys.update({ keyId, remaining: currentRemaining + creditsToAdd, refill: { interval: "monthly", amount: newCredits, }, meta: { ...currentKey?.meta, plan: newPlan, upgradedAt: new Date().toISOString(), }, }); // Option 2: Reset to new plan's full credits // await unkey.keys.update({ // keyId, // remaining: newCredits, // refill: { interval: "monthly", amount: newCredits }, // }); } ``` ## Multi-resource tracking Track different types of usage separately: ```typescript theme={null} // Create a key with multiple rate limits as credit pools const { data } = await unkey.keys.create({ apiId: process.env.UNKEY_API_ID!, externalId: customerId, remaining: 10000, // General API calls ratelimits: [ { name: "ai-tokens", limit: 100000, duration: 2592000000 }, // 30 days { name: "storage-mb", limit: 5000, duration: 2592000000 }, { name: "exports", limit: 100, duration: 2592000000 }, ], meta: { plan: "growth" }, }); // Check specific resource usage on verify const { data: verification } = await unkey.keys.verify({ key: userKey, ratelimits: [ { name: "ai-tokens", cost: tokenCount }, ], }); if (verification.ratelimits?.find(r => r.name === "ai-tokens")?.remaining === 0) { return { error: "AI token quota exceeded" }; } ``` ## Best practices Monthly refills keep usage tracking simple. For different billing cycles, adjust the interval. Don't just cut users off. Offer overage billing or soft limits with warnings. Let users see their usage in your dashboard. Nobody likes surprise bills. Use webhooks to keep Stripe/Paddle usage records in sync with Unkey. ## Next steps Complete Free/Pro/Enterprise implementation Deep dive into Unkey's credit system # Error Codes Source: https://unkey.com/docs/errors/overview Understanding Unkey's structured error system ## Introduction Unkey's error system uses a structured approach to organize and identify errors across the platform. This system makes it easier to understand, debug, and handle errors consistently. ## Error Code Format All Unkey error codes follow a consistent URN-like format: ``` err:system:category:specific ``` For example: `err:unkey:authentication:missing` This format breaks down as follows: * **err**: Standard prefix for all error codes * **system**: The service area or responsibility domain (e.g., unkey, user) * **category**: The error type or classification (e.g., authentication, data) * **specific**: The exact error condition (e.g., missing, malformed) ## Systems The "system" component identifies where the error originated: * **unkey**: Errors originating from Unkey's internal systems * **github**: Errors related to GitHub integration * **aws**: Errors related to AWS integration ## Categories The "category" component provides a second level of classification, for example: * **authentication**: Errors related to the authentication process * **authorization**: Errors related to permissions and access control * **application**: Errors related to application operations and system integrity * **data**: Errors related to data operations and resources * **limits**: Rate limiting or quota-related errors ## Error Response Format When an error occurs, the API returns a consistent JSON response format: ```json theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Authentication credentials were not provided", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/authentication/missing" } } ``` Key fields: * **requestId**: Unique identifier for the request (important for support) * **detail**: Human-readable explanation of the error * **status**: HTTP status code * **title**: Short summary of the error type * **type**: URL to detailed documentation about this error ## Documentation Integration All error codes have a corresponding documentation page accessible via the `type` URL in the error response. These pages provide detailed information about: * What caused the error * How to fix the issue * Common mistakes that lead to this error * Related errors you might encounter # assertion_failed Source: https://unkey.com/docs/errors/unkey/application/assertion_failed A runtime assertion or invariant check failed `err:unkey:application:invalid_input` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A system integrity check failed while processing your request", "status": 500, "title": "Internal Server Error", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/application/assertion_failed" } } ``` ## What Happened? This error occurs when Unkey's internal system detects an inconsistency or violation of its expected invariants during the processing of your request. Unlike validation errors which occur when your input is invalid, assertion failures happen when the system's internal state doesn't match what was expected. Possible causes include: * Data corruption or inconsistency in Unkey's database * Bugs in Unkey's business logic * Race conditions or timing issues * System state that violates core assumptions * Incompatible changes between different parts of the system This type of error is generally not caused by anything you did wrong in your request, but rather indicates an internal issue with Unkey's system integrity. ## How To Fix Since this is an internal system error, there's usually nothing you can directly do to fix it. However, you can try the following: 1. **Retry the request**: Some assertion failures may be due to temporary conditions that resolve themselves 2. **Contact Unkey support**: Report the error with the request ID to help Unkey address the underlying issue 3. **Check for workarounds**: In some cases, using a different API endpoint or approach might avoid the issue When contacting support, be sure to include: * The full error response, including the request ID * The API endpoint you were calling * The request payload (with sensitive information redacted) * Any patterns you've noticed (e.g., if it happens consistently or intermittently) ## Important Notes * Assertion failures indicate bugs or data integrity issues that Unkey needs to fix * Unlike many other errors, changing your request is unlikely to resolve the issue * These errors are typically logged and monitored by Unkey's engineering team * If you encounter this error consistently, there may be an underlying issue with your account data ## Related Errors * [err:unkey:application:unexpected\_error](./unexpected_error) - A more general internal error * [err:unkey:application:service\_unavailable](./service_unavailable) - When a service is temporarily unavailable # invalid_input Source: https://unkey.com/docs/errors/unkey/application/invalid_input Client provided input that failed validation `err:unkey:application:invalid_input` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The request contains invalid input that failed validation", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/application/invalid_input", "errors": [ { "location": "body.limit", "message": "must be greater than or equal to 1", "fix": "Provide a limit value of at least 1" } ] } } ``` ## What Happened? This error occurs when your request contains input data that doesn't meet Unkey's validation requirements. This could be due to missing required fields, values that are out of allowed ranges, incorrectly formatted data, or other validation failures. Common validation issues include: * Missing required fields * Values that exceed minimum or maximum limits * Strings that don't match required patterns * Invalid formats for IDs, emails, or other structured data * Type mismatches (e.g., providing a string where a number is expected) Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to create a rate limit with an invalid limit value of 0 curl -X POST https://api.unkey.com/v2/ratelimit.limit \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespace": "api.requests", "identifier": "user_123", "limit": 0, "duration": 60000 }' ``` ## How To Fix To fix this error, carefully review the error details provided in the response. The `errors` array contains specific information about what failed validation: 1. Check the `location` field to identify which part of your request is problematic 2. Read the `message` field for details about why validation failed 3. Look at the `fix` field (if available) for guidance on how to correct the issue 4. Modify your request to comply with the validation requirements Here's the corrected version of our example request: ```bash theme={null} # Corrected request with a valid limit value curl -X POST https://api.unkey.com/v2/ratelimit.limit \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespace": "api.requests", "identifier": "user_123", "limit": 100, "duration": 60000 }' ``` ## Common Mistakes * **Ignoring schema requirements**: Not checking the API documentation for field requirements * **Range violations**: Providing values outside of allowed ranges (too small, too large) * **Format errors**: Not following the required format for IDs, emails, or other structured data * **Missing fields**: Omitting required fields in API requests * **Type errors**: Sending the wrong data type (e.g., string instead of number) ## Related Errors * [err:unkey:application:assertion\_failed](./assertion_failed) - When a runtime assertion or invariant check fails * [err:unkey:application:protected\_resource](./protected_resource) - When attempting to modify a protected resource # precondition_failed Source: https://unkey.com/docs/errors/unkey/application/precondition_failed PreconditionFailed indicates a precondition check failed. `err:unkey:application:precondition_failed` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Vault hasn't been set up.", "status": 412, "title": "Precondition Failed", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/application/precondition_failed" } } ``` ## What Happened? This error occurs when your request is valid, but a precondition required to fulfill it is not met. Unlike validation errors where your input is invalid, precondition failures indicate that the system or resource is not configured correctly to handle your request. Common scenarios that trigger this error: * **API not configured for keys**: The API you're trying to create keys for doesn't have key authentication set up * **Vault not configured**: You're trying to create recoverable keys or decrypt keys, but the vault service isn't set up for your workspace * **Encryption not enabled**: You're requesting key encryption/decryption on an API that doesn't have encryption enabled * **Rate limit configuration missing**: You're checking a rate limit that doesn't exist for the key or its associated identity Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to create a recoverable key when vault isn't configured curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_ROOT_KEY" \ -d '{ "apiId": "api_1234567890", "prefix": "test", "recoverable": true }' ``` ## How To Fix The fix depends on which precondition failed. Check the error's `detail` field for specific information: ### Vault Not Set Up If you see "Vault hasn't been set up", you have two options: 1. **Configure the vault service** for your workspace (contact Unkey support if you need assistance) 2. **Remove the encryption requirement** from your request: ```bash theme={null} # Create a non-recoverable key instead curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_ROOT_KEY" \ -d '{ "apiId": "api_1234567890", "prefix": "test", "recoverable": false }' ``` ### API Not Set Up for Keys If you see "The requested API is not set up to handle keys", you need to: 1. Verify the API ID is correct 2. Ensure the API has key authentication configured in your Unkey dashboard 3. Create or configure the key authentication settings for your API ### API Not Set Up for Encryption If you see "This API does not support key encryption", either: 1. Enable encrypted key storage for the API in your Unkey dashboard settings 2. Remove the `recoverable: true` option or `decrypt: true` parameter from your request ### Rate Limit Not Found If you see a message about a requested rate limit not existing: 1. Verify the rate limit name is correct 2. Create the rate limit configuration for the key or its identity 3. Ensure the rate limit is associated with the correct resource ## Common Mistakes * **Assuming features are enabled by default**: Features like vault encryption and rate limits require explicit configuration * **Wrong API configuration**: Trying to use encryption features on an API that wasn't set up for it * **Missing rate limit setup**: Referencing rate limits that haven't been created yet * **Workspace-level configuration issues**: Some features need to be enabled at the workspace level before they can be used ## Related Errors * [err:unkey:application:invalid\_input](./invalid_input) - When your request input fails validation * [err:unkey:data:api\_not\_found](../data/api_not_found) - When the API itself doesn't exist * [err:unkey:application:service\_unavailable](./service_unavailable) - When a required service is temporarily unavailable # protected_resource Source: https://unkey.com/docs/errors/unkey/application/protected_resource Attempt to modify a protected resource `err:unkey:application:protected_resource` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The resource you are attempting to modify is protected and cannot be changed", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/application/protected_resource" } } ``` ## What Happened? This error occurs when you attempt to modify or delete a resource that is marked as protected in the Unkey system. Protected resources have a special status that prevents them from being changed or removed, typically because they are system resources, defaults, or otherwise critical to proper system operation. Common scenarios that trigger this error: * Attempting to delete a default API or workspace * Trying to modify system-created roles or permissions * Attempting to change protected settings or configurations * Trying to remove or alter resources that are required for system integrity Here's an example of a request that might trigger this error: ```bash theme={null} # Attempting to delete a protected default API curl -X DELETE https://api.unkey.com/v2/apis.deleteApi \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_default_protected" }' ``` ## How To Fix Since protected resources are deliberately shielded from modification, the solution is usually to work with or around them rather than trying to change them: 1. **Work with the protected resource**: Use the resource as-is and build your workflows around it 2. **Create a new resource**: Instead of modifying a protected resource, create a new one with your desired configuration 3. **Use alternatives**: Look for alternative ways to achieve your goal without modifying protected resources 4. **Contact support**: If you believe you have a legitimate need to modify a protected resource, contact Unkey support For example, instead of deleting a protected API, you might create a new one: ```bash theme={null} curl -X POST https://api.unkey.com/v2/apis.createApi \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "My Custom API" }' ``` ## Important Notes * Protected resources are designated as such for system stability and security reasons * Even with admin or owner permissions, protected resources typically cannot be modified * This protection is separate from permission-based restrictions and applies even to workspace owners * The protection status of a resource is not typically exposed in API responses until you try to modify it ## Related Errors * [err:unkey:authorization:forbidden](../authorization/forbidden) - When an operation is not allowed for policy reasons * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you lack permissions for an operation # service_unavailable Source: https://unkey.com/docs/errors/unkey/application/service_unavailable A service is temporarily unavailable `err:unkey:application:service_unavailable` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The service is temporarily unavailable. Please try again later.", "status": 503, "title": "Service Unavailable", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/application/service_unavailable" } } ``` ## What Happened? This error occurs when a component of the Unkey platform is temporarily unavailable or unable to process your request. Unlike an unexpected error, this is a known state where the system has detected that it cannot currently provide the requested service. Possible causes of this error: * Scheduled maintenance * High load or capacity issues * Dependent service outages * Regional infrastructure problems * Database overload or maintenance Here's an example of a request that might receive this error during a service disruption: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_123abc", "name": "My API Key" }' ``` ## How To Fix Since this is a temporary service issue, the best approach is to wait and retry. Here are some strategies: 1. **Implement retry logic**: Add automatic retries with exponential backoff to your code 2. **Check service status**: Visit the Unkey status page for updates on service availability 3. **Try alternate regions**: If Unkey offers region-specific endpoints, try an alternate region 4. **Wait and retry manually**: If it's a one-time operation, simply try again later Here's an example of a robust retry strategy: ```bash theme={null} # Bash script with retry logic max_attempts=5 attempt=0 backoff_time=1 while [ $attempt -lt $max_attempts ]; do response=$(curl -s -w "\n%{http_code}" \ -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_123abc", "name": "My API Key" }') http_code=$(echo "$response" | tail -n1) body=$(echo "$response" | sed '$ d') if [ $http_code -eq 503 ]; then attempt=$((attempt+1)) if [ $attempt -eq $max_attempts ]; then echo "Service still unavailable after $max_attempts attempts" exit 1 fi echo "Service unavailable, retrying in $backoff_time seconds... (Attempt $attempt/$max_attempts)" sleep $backoff_time backoff_time=$((backoff_time*2)) else echo "$body" exit 0 fi done ``` ## Important Notes * This error is temporary, and the service will typically recover automatically * For critical applications, implement circuit breakers to prevent cascading failures * If the service remains unavailable for an extended period, check Unkey's status page or contact support * Include the `requestId` from the error response when contacting support ## Related Errors * [err:unkey:application:unexpected\_error](./unexpected_error) - When an unhandled error occurs * [err:unkey:authorization:workspace\_disabled](../authorization/workspace_disabled) - When the workspace is disabled (a different type of unavailability) # unexpected_error Source: https://unkey.com/docs/errors/unkey/application/unexpected_error An unhandled or unexpected error occurred `err:unkey:application:unexpected_error` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "An unexpected error occurred while processing your request", "status": 500, "title": "Internal Server Error", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/application/unexpected_error" } } ``` ## What Happened? This error occurs when the Unkey system encounters an internal error that wasn't anticipated or couldn't be handled gracefully. This is generally not caused by anything you did wrong in your request, but rather indicates an issue within Unkey's systems. Possible causes of this error: * Temporary infrastructure issues * Database connectivity problems * Bugs in the Unkey service * Resource constraints or timeouts * Unexpected edge cases not handled by the application logic Here's an example of a request that might trigger this error if there's an internal issue: ```bash theme={null} # A valid request that could trigger an unexpected error if there's an internal issue curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_123abc", "name": "My API Key" }' ``` ## How To Fix Since this is an internal error, there's usually little you can do to directly fix it, but you can try the following: 1. **Retry the request**: Many unexpected errors are temporary and will resolve on a retry 2. **Check Unkey status**: Visit the Unkey status page to see if there are any ongoing service issues 3. **Contact support**: If the error persists, contact Unkey support with your request ID 4. **Implement retry logic**: For critical operations, implement exponential backoff retry logic in your code Here's an example of implementing retry logic with exponential backoff: ```javascript theme={null} // Pseudocode for retry with exponential backoff async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 300) { let retries = 0; while (true) { try { return await fn(); } catch (error) { if (error.status !== 500 || retries >= maxRetries) { throw error; // Either not a 500 error or we've exceeded retries } // Exponential backoff with jitter const delay = baseDelay * Math.pow(2, retries) * (0.8 + Math.random() * 0.4); console.log(`Retrying after ${delay}ms (attempt ${retries + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); retries++; } } } ``` ## Important Notes * Always include the `requestId` when contacting support about this error * This error may indicate a bug in Unkey's systems that needs to be fixed * Unlike most other errors, this one usually can't be resolved by changing your request * If you encounter this error consistently with a specific API call, there may be an edge case that Unkey's team needs to address ## Related Errors * [err:unkey:application:service\_unavailable](./service_unavailable) - When a service is temporarily unavailable # key_not_found Source: https://unkey.com/docs/errors/unkey/authentication/key_not_found The authentication key was not found `err:unkey:authentication:key_not_found` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The provided API key was not found", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/authentication/key_not_found" } } ``` ## What Happened? This error occurs when you've provided a properly formatted API key in your request to the Unkey API, but the key doesn't exist in Unkey's system. The key might have been deleted, revoked, or you might be using an incorrect key. Common causes include: * Using an API key that has been deleted * Using an API key from a different workspace or environment * Typographical errors when entering the key * Using a test key in production or vice versa Here's an example of a request with a non-existent API key: ```bash theme={null} # Request to Unkey API with a non-existent key curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_NONEXISTENT_KEY" ``` ## How To Fix To fix this error, you need to use a valid API key when making requests to the Unkey API: 1. **Check your Unkey dashboard**: Verify you're using the correct Unkey API key from the [Unkey dashboard](https://app.unkey.com) 2. **Create a new key if needed**: If your key was deleted, create a new one 3. **Use the correct environment**: Make sure you're using the appropriate key for your environment (development, production, etc.) Here's how to check and use the correct Unkey API key: 1. Log in to your Unkey dashboard 2. Navigate to the API keys section 3. Copy the appropriate API key for your environment 4. Use the key in your request as shown below ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_VALID_API_KEY" ``` ## Common Mistakes * **Using revoked Unkey keys**: API keys that have been revoked will return this error * **Environment mismatch**: Using development keys in production or vice versa * **Workspace confusion**: Using keys from one workspace in another workspace's API calls * **Copy-paste errors**: Inadvertently omitting part of the key when copying * **Expired keys**: Keys that have expired will return this error * **Using demo keys**: Using example keys from documentation ## Related Errors * [err:unkey:authentication:missing](./missing) - When no authentication credentials are provided * [err:unkey:authentication:malformed](./malformed) - When the API key is provided but formatted incorrectly # malformed Source: https://unkey.com/docs/errors/unkey/authentication/malformed Authentication credentials were incorrectly formatted `err:unkey:authentication:malformed` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Authentication credentials were incorrectly formatted", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/authentication/malformed" } } ``` ## What Happened? This error occurs when your request includes authentication credentials, but they are not formatted correctly. The Unkey API expects API keys to be provided in a specific format in the Authorization header. Common causes include: * Missing the "Bearer" prefix before your API key * Including extra spaces or characters * Using incorrect casing (e.g., "bearer" instead of "Bearer") * Providing a malformed or truncated API key Here's an example of a request with incorrectly formatted credentials: ```bash theme={null} # Missing the "Bearer" prefix curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: unkey_YOUR_API_KEY" ``` ## How To Fix To fix this error, ensure your Authorization header follows the correct format: 1. **Use the correct format**: Ensure your Authorization header follows the format `Bearer unkey_YOUR_API_KEY` 2. **Check for extra spaces or characters**: Make sure there are no invisible characters or line breaks 3. **Verify the API key format**: Your Unkey API key should start with `unkey_` Here's the correctly formatted request: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` When properly authenticated, you'll receive a successful response like this: ```json theme={null} { "meta": { "requestId": "req_8f7g6h5j4k3l2m1n" }, "data": { "keys": [ { "keyId": "key_123abc456def", "name": "Production API Key" } ] } } ``` ## Common Mistakes * **Authorization header format**: Must be exactly `Bearer unkey_YOUR_API_KEY` with a single space after "Bearer" * **Incorrect casing**: Using "bearer" instead of "Bearer" * **API key format**: Your Unkey API key should start with `unkey_` and contain no spaces * **Using wrong key type**: Ensure you're using a root key for management API calls * **Copying errors**: Check for invisible characters or line breaks that might have been copied * **Extra characters**: Including quotes or other characters around the API key * **Truncated keys**: Accidentally cutting off part of the API key when copying ## Related Errors * [err:unkey:authentication:missing](./missing) - When no authentication credentials are provided * [err:unkey:authentication:key\_not\_found](./key_not_found) - When the provided API key doesn't exist # missing Source: https://unkey.com/docs/errors/unkey/authentication/missing Authentication credentials were not provided in the request `err:unkey:authentication:missing` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Authentication credentials were not provided", "status": 401, "title": "Unauthorized", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/authentication/missing" } } ``` ## What Happened? This error occurs when you make a request to the Unkey API without including your API key in the Authorization header. The Unkey API requires authentication for most endpoints to verify your identity and permissions. Here's an example of a request that would trigger this error: ```bash theme={null} # Request to Unkey API without an API key curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" ``` Authentication is required to: * Verify your identity * Ensure you have permission to perform the requested operation * Track usage and apply appropriate rate limits * Maintain security and audit trails ## How To Fix To fix this error, you need to include your Unkey API key in the Authorization header of your request: 1. **Get your Unkey API key**: Obtain your API key from the [Unkey dashboard](https://app.unkey.com) 2. **Add the Authorization header**: Include your Unkey API key with the format `Bearer unkey_YOUR_API_KEY` Here's the corrected request: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` When properly authenticated, you'll receive a successful response like this: ```json theme={null} { "meta": { "requestId": "req_8f7g6h5j4k3l2m1n" }, "data": { "keys": [ { "keyId": "key_123abc456def", "name": "Production API Key" } ] } } ``` ## Common Mistakes * **Missing the `Bearer` prefix**: Unkey requires the format `Bearer unkey_YOUR_API_KEY` with a space after "Bearer" * **Headers lost in proxies**: Some proxy servers or API sentinels might strip custom headers * **Expired or revoked keys**: Using keys that are no longer valid * **Wrong environment**: Using development keys in production or vice versa ## Related Errors * [err:unkey:authentication:malformed](./malformed) - When the API key is provided but formatted incorrectly * [err:unkey:authentication:key\_not\_found](./key_not_found) - When the provided API key doesn't exist # forbidden Source: https://unkey.com/docs/errors/unkey/authorization/forbidden The operation is not allowed `err:unkey:authorization:forbidden` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "This operation is not allowed", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/authorization/forbidden" } } ``` ## What Happened? This error occurs when you attempt an operation that is prohibited by Unkey's platform policies, even if your API key has high-level permissions. Unlike the "insufficient\_permissions" error which relates to permission roles, this error indicates that the operation itself is not allowed regardless of permissions. Common scenarios that trigger this error: * Trying to perform operations on protected or system resources * Attempting to modify resources that are in a state that doesn't allow modifications * Trying to exceed account limits or quotas * Performing operations that violate platform policies Here's an example of a request that might trigger this error: ```bash theme={null} # Attempting to delete a protected system resource curl -X POST https://api.unkey.com/v2/apis.deleteApi \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_ADMIN_KEY" \ -d '{ "apiId": "api_system_protected" }' ``` ## How To Fix This error indicates a fundamental restriction rather than a permission issue. The operation you're trying to perform may be: 1. **Not supported by the Unkey platform**: Some operations are simply not available 2. **Blocked due to your account's current state or limitations**: Your account may not have access to certain features 3. **Prevented by safety mechanisms**: System protections may prevent certain destructive operations Possible solutions include: * **Check Unkey's documentation**: Understand which operations have fundamental restrictions * **Consider your account state**: Some operations may be blocked due to your account state or plan * **Use alternative approaches**: Find supported ways to accomplish similar goals * **If you're trying to modify a resource in a specific state**, check if it needs to be in a different state first * **If you're hitting account limits**, consider upgrading your plan * **Contact Unkey support** if you believe this restriction shouldn't apply to your use case ## Common Mistakes * **Attempting to modify system resources**: Some Unkey resources are protected and cannot be modified * **Order-dependent operations**: Trying to perform operations out of their required sequence * **Plan limitations**: Attempting operations not available on your current plan * **Resource state issues**: Trying to modify resources that are in a state that doesn't allow changes * **Ignoring documentation warnings**: Not reading warnings about restricted operations * **Testing security boundaries**: Deliberately trying to access protected resources * **Outdated documentation**: Following outdated documentation that suggests now-forbidden operations ## Related Errors * [err:unkey:authorization:insufficient\_permissions](./insufficient_permissions) - When the authenticated entity lacks specific permissions * [err:unkey:authorization:key\_disabled](./key_disabled) - When the authentication key is disabled * [err:unkey:authorization:workspace\_disabled](./workspace_disabled) - When the associated workspace is disabled # insufficient_permissions Source: https://unkey.com/docs/errors/unkey/authorization/insufficient_permissions The authenticated entity lacks sufficient permissions for the requested operation `err:unkey:authorization:insufficient_permissions` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The authenticated API key does not have permission to perform this operation", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/authorization/insufficient_permissions" } } ``` ## What Happened? This error occurs when your API key is valid and properly authenticated, but it doesn't have the necessary permissions to perform the requested operation. In Unkey, different API keys can have different permission levels. Common scenarios that trigger this error: * Using a read-only key to perform write operations * Using a key limited to specific resources to access other resources * Attempting to access resources across workspaces with a workspace-scoped key * Using a key with limited permissions to perform administrative actions Here's an example of a request using a key with insufficient permissions: ```bash theme={null} # Using a read-only key to create a new API key curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_READ_ONLY_KEY" \ -d '{ "apiId": "api_123", "name": "New API Key" }' ``` ## How To Fix You need to use an API key with the appropriate permissions for the operation you're trying to perform. Here are some steps to resolve this issue: 1. **Check permissions**: Verify the permissions of your current Unkey API key in the [Unkey dashboard](https://app.unkey.com) 2. **Create a new key**: If needed, create a new Unkey API key with the required permissions 3. **Use role-based keys**: Consider using separate keys for different operations based on their permission requirements Here's an example using a key with the appropriate permissions: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_ADMIN_KEY" \ -d '{ "apiId": "api_123", "name": "New API Key" }' ``` ## Common Mistakes * **Using development keys in production**: Keys may have different permissions across environments * **Mixing key scopes**: Using a key scoped to one resource to access another * **Role misunderstanding**: Not understanding the specific permissions granted to each role * **Workspace boundaries**: Attempting to cross workspace boundaries with a limited key * **Permission level confusion**: Not understanding what operations require elevated permissions * **Expired or downgraded privileges**: Using a key whose permissions have been reduced since it was issued ## Related Errors * [err:unkey:authorization:forbidden](./forbidden) - When the operation is not allowed for policy reasons * [err:unkey:authorization:key\_disabled](./key_disabled) - When the authentication key is disabled # key_disabled Source: https://unkey.com/docs/errors/unkey/authorization/key_disabled The authentication key is disabled `err:unkey:authorization:key_disabled` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The API key used for authentication has been disabled", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/authorization/key_disabled" } } ``` ## What Happened? This error occurs when you try to use a disabled Unkey API key (one that starts with `unkey_`) to authenticate with the Unkey API. The key exists in the system but has been disabled and can no longer be used for authentication. Here's an example of a request that would trigger this error: ```bash theme={null} # Request to Unkey API with a disabled key curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_DISABLED_KEY" ``` API keys can be disabled for various reasons: * Administrative action to revoke access * Security concerns or suspected compromise * Temporary deactivation during maintenance or investigation * Automated disabling due to suspicious activity * Usage policy violations ## How To Fix If you encounter this error when using the Unkey API, you have two options: 1. **Get a new Unkey root key**: If your key was permanently disabled, create a new API key with the appropriate permissions in the [Unkey dashboard](https://app.unkey.com/settings/root-keys) 2. **Re-enable your existing key**: If you have administrative access and the key was temporarily disabled, you can re-enable it through the dashboard To re-enable your Unkey root key: 1. Log in to your Unkey dashboard 2. Navigate to the API keys section 3. Search for the key you want to re-enable 4. Click "Enable" Then update your API calls to use the re-enabled key: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_REACTIVATED_KEY" ``` ## Common Mistakes * **Using old or archived root keys**: Keys from previous projects or configurations may have been disabled * **Shared root keys**: When keys are shared among team members, they may be disabled by another administrator * **Security triggers**: Unusual usage patterns may automatically disable keys as a security precaution * **Environment confusion**: Using disabled staging/development keys in production environments * **Account status changes**: Keys may be disabled due to billing or account status changes * **Rotation policies**: Keys that should have been rotated according to security policies ## Related Errors * [err:unkey:authorization:insufficient\_permissions](./insufficient_permissions) - When the authenticated entity lacks sufficient permissions * [err:unkey:authorization:workspace\_disabled](./workspace_disabled) - When the associated workspace is disabled * [err:unkey:authentication:key\_not\_found](../authentication/key_not_found) - When the provided API key doesn't exist at all # workspace_disabled Source: https://unkey.com/docs/errors/unkey/authorization/workspace_disabled The associated workspace is disabled `err:unkey:authorization:workspace_disabled` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The workspace associated with this API key has been disabled", "status": 403, "title": "Forbidden", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/authorization/workspace_disabled" } } ``` ## What Happened? This error occurs when you attempt to use an Unkey API key that belongs to a disabled workspace. When a workspace is disabled in Unkey, all API keys associated with that workspace stop working, regardless of their individual status. Here's an example of a request that would trigger this error: ```bash theme={null} # Request to Unkey API with a key from a disabled workspace curl -X POST https://api.unkey.com/v2/keys.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_KEY_FROM_DISABLED_WORKSPACE" ``` A workspace might be disabled for various reasons: * Billing issues or unpaid invoices * Administrative action due to terms of service violations * At the workspace owner's request * During investigation of suspicious activity * As part of account closure process * Exceeding usage limits or quotas ## How To Fix If you encounter this error when using the Unkey API, you need to address the workspace issue: 1. **Check billing status**: If the workspace was disabled due to billing issues, settle any outstanding payments in the [Unkey dashboard](https://app.unkey.com/settings/billing) 2. **Contact workspace administrator**: If you're not the workspace administrator, contact them to determine why the workspace was disabled 3. **Contact Unkey support**: If you believe the workspace was disabled in error, or you need assistance resolving the issue, contact [Unkey support](mailto:support@unkey.com) 4. **Use a key from a different workspace**: If you have access to multiple workspaces, you can temporarily use a key from an active workspace while resolving the issue Once the workspace is re-enabled, all API keys associated with it should become usable again (unless individually disabled). ## Common Mistakes * **Billing oversights**: Missed payment notifications can lead to workspace suspension * **Usage violations**: Excessive usage or pattern violations may trigger workspace disabling * **Administrative changes**: Organizational changes might lead to workspaces being temporarily disabled * **Using old workspaces**: Attempting to use keys from deprecated or archived workspaces * **Plan limitation violations**: Exceeding the limits of your current plan * **Account transfer issues**: Workspaces may be temporarily disabled during ownership transfers ## Related Errors * [err:unkey:authorization:key\_disabled](./key_disabled) - When the specific authentication key is disabled * [err:unkey:authorization:insufficient\_permissions](./insufficient_permissions) - When the authenticated entity lacks sufficient permissions * [err:unkey:data:workspace\_not\_found](../data/workspace_not_found) - When the requested workspace doesn't exist # analytics_connection_failed Source: https://unkey.com/docs/errors/unkey/data/analytics_connection_failed ConnectionFailed indicates the connection to the analytics database failed. `err:unkey:data:analytics_connection_failed` # analytics_not_configured Source: https://unkey.com/docs/errors/unkey/data/analytics_not_configured NotConfigured indicates analytics is not configured for the workspace. `err:unkey:data:analytics_not_configured` # api_not_found Source: https://unkey.com/docs/errors/unkey/data/api_not_found The requested API was not found err:unkey:data:api\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested API could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/api_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on an API that doesn't exist in the Unkey system. In Unkey, APIs are resources that you create to organize and manage your keys. Common scenarios that trigger this error: * Using an incorrect API ID in your requests * Referencing an API that has been deleted * Attempting to access an API in a workspace you don't have access to * Typos in API names when using name-based lookups Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to create a key for a non-existent API curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_nonexistent", "name": "hello world" }' ``` ## How To Fix Verify that you're using the correct API ID and that the API still exists in your workspace: 1. List all APIs in your workspace to find the correct ID 2. Check if the API has been deleted and recreate it if necessary 3. Verify you're working in the correct workspace 4. Ensure proper permissions to access the API Here's how to list all APIs in your workspace: ```bash theme={null} curl -X POST https://api.unkey.com/v2/apis.listApis \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` If you need to create a new API, use the `apis.createApi` endpoint: ```bash theme={null} curl -X POST https://api.unkey.com/v2/apis.createApi \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "My New API" }' ``` ## Common Mistakes * **Copy-paste errors**: Using incorrect API IDs from documentation examples * **Deleted APIs**: Attempting to reference APIs that have been deleted * **Environment confusion**: Looking for an API in production that only exists in development * **Workspace boundaries**: Trying to access an API that exists in another workspace ## Related Errors * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist * [err:unkey:data:key\_not\_found](./key_not_found) - When the requested key doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to access an API # audit_log_not_found Source: https://unkey.com/docs/errors/unkey/data/audit_log_not_found The requested audit log was not found err:unkey:data:audit\_log\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested audit log could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/audit_log_not_found" } } ``` ## What Happened? This error occurs when you're trying to retrieve or operate on a specific audit log entry that doesn't exist in the Unkey system. Audit logs record important actions and events that occur within your workspace. Common scenarios that trigger this error: * Using an incorrect audit log ID * Requesting an audit log entry that has been deleted or expired * Trying to access audit logs from a different workspace * Typographical errors in audit log identifiers Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to get a non-existent audit log entry curl -X POST https://api.unkey.com/v2/audit.getLog \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "logId": "log_nonexistent" }' ``` ## How To Fix Verify that you're using the correct audit log ID and that the log entry still exists in your workspace: 1. Check the audit log ID in your request for typos or formatting errors 2. Use the list audit logs endpoint to find valid log IDs 3. Verify you're working in the correct workspace 4. Consider that audit logs might have a retention period after which they're automatically deleted Here's how to list recent audit logs in your workspace: ```bash theme={null} curl -X POST https://api.unkey.com/v2/audit.listLogs \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "limit": 10 }' ``` ## Common Mistakes * **Expired logs**: Trying to access audit logs beyond the retention period * **Copy-paste errors**: Using incorrect log IDs from documentation examples * **Workspace boundaries**: Attempting to access logs from another workspace * **Permission issues**: Trying to access logs you don't have permission to view ## Related Errors * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to access audit logs * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist # identity_already_exists Source: https://unkey.com/docs/errors/unkey/data/identity_already_exists The requested identity already exists err:unkey:data:identity\_already\_exists ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "An identity with this external ID already exists", "status": 409, "title": "Conflict", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/identity_already_exists" } } ``` ## What Happened? This error occurs when you're trying to create an identity with an external ID that already exists in your Unkey workspace. External IDs must be unique within a workspace to avoid confusion and maintain data integrity. Common scenarios that trigger this error: * Creating an identity with an external ID that's already in use * Re-creating a previously deleted identity with the same external ID * Migration or import processes that don't check for existing identities * Duplicate API calls due to retries or network issues Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to create an identity with an external ID that already exists curl -X POST https://api.unkey.com/v2/identities.createIdentity \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "externalId": "user_123", "meta": { "name": "John Doe", "email": "john@example.com" } }' ``` ## How To Fix When you encounter this error, you have several options: 1. **Use a different external ID**: If creating a new identity, use a unique external ID 2. **Update the existing identity**: If you want to modify an existing identity, use the update endpoint instead 3. **Get the existing identity**: If you just need the identity information, retrieve it rather than creating it 4. **Implement upsert logic**: Use a get-or-create pattern in your code Here's how to update an existing identity: ```bash theme={null} curl -X POST https://api.unkey.com/v2/identities.updateIdentity \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "externalId": "user_123", "meta": { "name": "John Doe", "email": "updated_email@example.com" } }' ``` Or implement a get-or-create pattern in your code: ```javascript theme={null} // Pseudocode for get-or-create pattern async function getOrCreateIdentity(externalId, meta) { try { // Try to create the identity return await createIdentity(externalId, meta); } catch (error) { // If it already exists (409 error), get it instead if (error.status === 409) { return await getIdentity(externalId); } // Otherwise, rethrow the error throw error; } } ``` ## Common Mistakes * **Not checking for existing identities**: Failing to check if an identity already exists before creating it * **Retry loops**: Repeatedly trying to create the same identity after a failure * **Case sensitivity**: Not accounting for case sensitivity in external IDs * **Cross-environment duplication**: Using the same external IDs across development and production environments ## Related Errors * [err:unkey:data:identity\_not\_found](./identity_not_found) - When the requested identity doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on identities # identity_not_found Source: https://unkey.com/docs/errors/unkey/data/identity_not_found The requested identity was not found err:unkey:data:identity\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested identity could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/identity_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on an identity that doesn't exist in the Unkey system. Identities in Unkey are used to represent users or entities that own or use API keys. Common scenarios that trigger this error: * Using an incorrect identity ID or external ID * Referencing an identity that has been deleted * Trying to update or get information about a non-existent identity * Typos in identity identifiers Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to get a non-existent identity curl -X POST https://api.unkey.com/v2/identities.getIdentity \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "identityId": "ident_nonexistent" }' ``` ## How To Fix Verify that you're using the correct identity ID or external ID and that the identity still exists in your workspace: 1. Check the identity ID in your request for typos or formatting errors 2. List all identities in your workspace to find the correct ID 3. If the identity has been deleted, you may need to recreate it If you need to create a new identity, use the `identities.createIdentity` endpoint: ```bash theme={null} curl -X POST https://api.unkey.com/v2/identities.createIdentity \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "externalId": "user_123", "meta": { "name": "John Doe", "email": "john@example.com" } }' ``` ## Common Mistakes * **Incorrect identifiers**: Using wrong identity IDs or external IDs * **Deleted identities**: Attempting to reference identities that have been removed * **Case sensitivity**: External IDs might be case-sensitive * **Workspace boundaries**: Trying to access identities from another workspace ## Related Errors * [err:unkey:data:identity\_already\_exists](./identity_already_exists) - When trying to create an identity that already exists * [err:unkey:data:key\_not\_found](./key_not_found) - When the requested key doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on identities # key_auth_not_found Source: https://unkey.com/docs/errors/unkey/data/key_auth_not_found The requested key authentication was not found err:unkey:data:key\_auth\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested key authentication could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/key_auth_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a key authentication record that doesn't exist in the Unkey system. Key authentication records contain information about how API keys are authenticated. Common scenarios that trigger this error: * Using an incorrect key authentication ID * Referencing a key authentication record that has been deleted * Attempting to update authentication settings for a non-existent record * Typos in identifiers ## How To Fix Verify that you're using the correct key authentication ID and that the record still exists: 1. Check the key authentication ID in your request for typos or formatting errors 2. Verify the key authentication record exists by looking up the associated key 3. If the record has been deleted, you may need to recreate the key or its authentication settings Here's how to get information about a key's authentication settings: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.getKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "keyId": "key_your_key_id" }' ``` ## Common Mistakes * **Copy-paste errors**: Incorrect IDs due to copy-paste mistakes * **Deleted records**: Attempting to reference authentication records for deleted keys * **Misunderstanding relationships**: Confusing key IDs with key authentication IDs * **Workspace boundaries**: Trying to access authentication records from another workspace ## Related Errors * [err:unkey:data:key\_not\_found](./key_not_found) - When the requested key doesn't exist * [err:unkey:authentication:key\_not\_found](../authentication/key_not_found) - When an API key used for authentication doesn't exist * [err:unkey:authorization:key\_disabled](../authorization/key_disabled) - When the authentication key is disabled # key_not_found Source: https://unkey.com/docs/errors/unkey/data/key_not_found The requested key was not found err:unkey:data:key\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested API key could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/key_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a specific API key using its ID, but the key with that ID doesn't exist in the system. This is different from the authentication error `err:unkey:authentication:key_not_found`, which occurs during the authentication process. Common scenarios that trigger this error: * Attempting to update, delete, or get information about a key that has been deleted * Using an incorrect or malformed key ID * Trying to access a key that exists in a different workspace * Reference to a key that hasn't been created yet Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to get details for a non-existent key curl -X POST https://api.unkey.com/v2/keys.getKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "keyId": "key_nonexistent" }' ``` ## How To Fix Verify that you're using the correct key ID and that the key still exists in your workspace: 1. Check the key ID in your request for typos or formatting errors 2. Confirm the key exists by listing all keys in your workspace via the [Unkey dashboard](https://unkey.com/dashboard) or the API 3. Verify you're working in the correct workspace 4. If you need to create a new key, use the `keys.createKey` endpoint Here's how to list all keys to find the correct ID: ```bash theme={null} curl -X POST https://api.unkey.com/v2/apis.listKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "apiId": "api_your_api_id" }' ``` ## Common Mistakes * **Copy-paste errors**: Incorrect key IDs due to copy-paste mistakes * **Deleted keys**: Attempting to reference keys that have been deleted * **Environment confusion**: Looking for a key in production that only exists in development * **Workspace boundaries**: Trying to access a key that exists in another workspace ## Related Errors * [err:unkey:authentication:key\_not\_found](../authentication/key_not_found) - When an API key used for authentication doesn't exist * [err:unkey:data:api\_not\_found](./api_not_found) - When the requested API doesn't exist * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist # key_space_not_found Source: https://unkey.com/docs/errors/unkey/data/key_space_not_found NotFound indicates the requested key space was not found. `err:unkey:data:key_space_not_found` # migration_not_found Source: https://unkey.com/docs/errors/unkey/data/migration_not_found The requested Key Migration was not found err:unkey:data:migration\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested Migration could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/migration_not_found" } } ``` ## What Happened? This error occurs when you're trying to migrate api keys for a migration that doesn't exist in the Unkey system. Common scenarios that trigger this error: * Using an incorrect or expired migrationId * The migration was deleted * The migration belongs to a different workspace * Typos in the migrationId Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to migrate keys with a non-existent migrationId curl -X POST https://api.unkey.com/v2/keys.migrateKeys \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "apiId": "api_123", "migrationId": "migration_456", "keys": [{ "hash": "deadbeef" }] }' ``` ## How To Fix If you’re unsure about your migrationId or setup, contact [support@unkey.com](mailto:support@unkey.com). # permission_already_exists Source: https://unkey.com/docs/errors/unkey/data/permission_already_exists A permission with this slug already exists err:unkey:data:permission\_already\_exists ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A permission with slug \"admin\" already exists in this workspace", "status": 409, "title": "Conflict", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/permission/duplicate" } } ``` ## What Happened? This error occurs when you're trying to create a permission with a name that already exists in your Unkey workspace. Permission names must be unique within a workspace to avoid confusion and maintain proper access control. Common scenarios that trigger this error: * Creating a permission with a name that's already in use * Re-creating a previously deleted permission with the same name * Migration or import processes that don't check for existing permissions * Duplicate API calls due to retries or network issues Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to create a permission with a name that already exists curl -X POST https://api.unkey.com/v2/permissions.createPermission \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "admin", "slug": "admin-access", "description": "Administrator access" }' ``` ## How To Fix When you encounter this error, you have several options: 1. **Use a different name**: If creating a new permission, use a unique name 2. **Get the existing permission**: If you just need the permission information, retrieve it rather than creating it 3. **List existing permissions**: Check what permissions already exist before creating new ones 4. **Implement idempotent creation**: Use a get-or-create pattern in your code Here's how to list existing permissions: ```bash theme={null} curl -X POST https://api.unkey.com/v2/permissions.listPermissions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{}' ``` Or implement a get-or-create pattern in your code: ```javascript theme={null} // Pseudocode for get-or-create pattern async function getOrCreatePermission(name, slug, description) { try { // Try to create the permission return await createPermission(name, slug, description); } catch (error) { // If it already exists (409 error), get it instead if (error.status === 409) { // Extract the permission name from the error message and get it const permissions = await listPermissions(); return permissions.find( (p) => p.name.toLowerCase() === name.toLowerCase() ); } // Otherwise, rethrow the error throw error; } } ``` ## Common Mistakes * **Not checking for existing permissions**: Failing to check if a permission already exists before creating it * **Case sensitivity**: Permission names are case-insensitive - "Admin" and "admin" are the same * **Retry loops**: Repeatedly trying to create the same permission after a failure * **Cross-environment duplication**: Using the same permission names across development and production without proper namespacing ## Related Errors * [err:unkey:data:permission\_not\_found](./permission_not_found) - When the requested permission doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to manage permissions # permission_not_found Source: https://unkey.com/docs/errors/unkey/data/permission_not_found The requested permission was not found err:unkey:data:permission\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested permission could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/permission_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a permission that doesn't exist in the Unkey system. Permissions in Unkey are used to control access to resources and operations. Common scenarios that trigger this error: * Using an incorrect permission ID or name * Referencing a permission that has been deleted * Trying to assign a permission that doesn't exist in the current workspace * Typos in permission names when using name-based lookups ## How To Fix Verify that you're using the correct permission ID or name and that the permission still exists in your workspace: 1. List all permissions in your workspace to find the correct ID 2. Check if the permission has been deleted and recreate it if necessary 3. Verify you're working in the correct workspace Here's how to list all permissions in your workspace: ```bash theme={null} curl -X POST https://api.unkey.com/v2/permissions.listPermissions \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` If you need to create a new permission, use the appropriate API endpoint: ```bash theme={null} curl -X POST https://api.unkey.com/v2/permissions.createPermission \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "read:keys", "description": "Allows reading key information" }' ``` ## Common Mistakes * **Incorrect identifiers**: Using wrong permission IDs or names * **Deleted permissions**: Referencing permissions that have been removed * **Case sensitivity**: Permissions names might be case-sensitive * **Workspace boundaries**: Trying to use permissions from another workspace ## Related Errors * [err:unkey:data:role\_not\_found](./role_not_found) - When the requested role doesn't exist * [err:unkey:data:api\_not\_found](./api_not_found) - When the requested API doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on permissions # Ratelimit Namespace Gone Source: https://unkey.com/docs/errors/unkey/data/ratelimit_namespace_gone ## Description This error occurs when you attempt to use a ratelimit namespace that has been deleted. Once a namespace is deleted, it cannot be restored through the API or dashboard. ## Error Code `unkey/data/ratelimit_namespace_gone` ## HTTP Status Code `410 Gone` ## Cause The ratelimit namespace you're trying to access was previously deleted and is no longer available through the API or dashboard. ## Resolution Contact [support@unkey.com](mailto:support@unkey.com) with your workspace ID and namespace name if you need this namespace restored. ## Prevention To avoid accidentally deleting namespaces: * Restrict namespace deletion via workspace permissions * Carefully review namespace-deletion requests before confirming ## Related * [Ratelimit Namespace Not Found](/errors/unkey/data/ratelimit_namespace_not_found) * [Rate Limiting Documentation](/ratelimiting/introduction) # ratelimit_namespace_not_found Source: https://unkey.com/docs/errors/unkey/data/ratelimit_namespace_not_found The requested rate limit namespace was not found err:unkey:data:ratelimit\_namespace\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested rate limit namespace could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/ratelimit_namespace_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a rate limit namespace that doesn't exist in the Unkey system. Rate limit namespaces are used to organize and manage rate limits for different resources or operations. Common scenarios that trigger this error: * Using an incorrect namespace ID or name * Referencing a namespace that has been deleted * Trying to modify a namespace that doesn't exist in the current workspace * Typos in namespace names when using name-based lookups Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to get overrides for a non-existent namespace curl -X POST https://api.unkey.com/v2/ratelimit.listOverrides \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespaceName": "nonexistent_namespace" }' ``` ## How To Fix Verify that you're using the correct namespace ID or name and that the namespace still exists in your workspace: 1. Check the namespace ID or name in your request for typos or formatting errors 2. List all namespaces in your workspace to find the correct ID or name 3. If the namespace has been deleted, you may need to recreate it Here's how to use the correct namespace in a rate limit operation: ```bash theme={null} # Creating a rate limit using a valid namespace curl -X POST https://api.unkey.com/v2/ratelimit.limit \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespace": "your_valid_namespace", "identifier": "user_123", "limit": 100, "duration": 60000 }' ``` ## Common Mistakes * **Typos in namespace names**: Small typographical errors in namespace names * **Case sensitivity**: Namespace names might be case-sensitive * **Deleted namespaces**: Referencing namespaces that have been removed * **Workspace boundaries**: Trying to use namespaces from another workspace ## Related Errors * [err:unkey:data:ratelimit\_override\_not\_found](./ratelimit_override_not_found) - When the requested rate limit override doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on rate limits * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist # ratelimit_override_not_found Source: https://unkey.com/docs/errors/unkey/data/ratelimit_override_not_found The requested rate limit override was not found err:unkey:data:ratelimit\_override\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested rate limit override could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/ratelimit_override_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a rate limit override that doesn't exist in the Unkey system. Rate limit overrides are used to create custom rate limits for specific identifiers within a namespace. Common scenarios that trigger this error: * Using an incorrect override ID * Referencing an override that has been deleted * Trying to get or modify an override for an identifier that doesn't have one * Using the wrong namespace when looking up an override Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to get a non-existent rate limit override curl -X POST https://api.unkey.com/v2/ratelimit.getOverride \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespaceId": "ns_123abc", "identifier": "user_without_override" }' ``` ## How To Fix Verify that you're using the correct namespace and identifier, and that the override still exists: 1. Check the namespace ID and identifier in your request for typos 2. List all overrides in the namespace to confirm if the one you're looking for exists 3. If the override has been deleted or never existed, you may need to create it Here's how to list overrides in a namespace: ```bash theme={null} curl -X POST https://api.unkey.com/v2/ratelimit.listOverrides \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespaceId": "ns_123abc" }' ``` If you need to create a new override, use the `ratelimit.setOverride` endpoint: ```bash theme={null} curl -X POST https://api.unkey.com/v2/ratelimit.setOverride \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "namespaceId": "ns_123abc", "identifier": "user_123", "limit": 200, "duration": 60000 }' ``` ## Common Mistakes * **Wrong identifier**: Using an incorrect user identifier when looking up overrides * **Deleted overrides**: Attempting to reference overrides that have been removed * **Namespace mismatch**: Looking in the wrong namespace for an override * **Assuming defaults are overrides**: Trying to get an override for an identifier that's using default limits ## Related Errors * [err:unkey:data:ratelimit\_namespace\_not\_found](./ratelimit_namespace_not_found) - When the requested rate limit namespace doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on rate limit overrides * [err:unkey:data:workspace\_not\_found](./workspace_not_found) - When the requested workspace doesn't exist # role_already_exists Source: https://unkey.com/docs/errors/unkey/data/role_already_exists A role with this name already exists err:unkey:data:role\_already\_exists ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "A role with name \"admin\" already exists in this workspace", "status": 409, "title": "Conflict", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/role/duplicate" } } ``` ## What Happened? This error occurs when you're trying to create a role with a name that already exists in your Unkey workspace. Role names must be unique within a workspace to avoid confusion and maintain proper access control. Common scenarios that trigger this error: * Creating a role with a name that's already in use * Re-creating a previously deleted role with the same name * Migration or import processes that don't check for existing roles * Duplicate API calls due to retries or network issues Here's an example of a request that would trigger this error: ```bash theme={null} # Attempting to create a role with a name that already exists curl -X POST https://api.unkey.com/v2/permissions.createRole \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "admin", "description": "Administrator role with full access" }' ``` ## How To Fix When you encounter this error, you have several options: 1. **Use a different name**: If creating a new role, use a unique name 2. **Get the existing role**: If you just need the role information, retrieve it rather than creating it 3. **List existing roles**: Check what roles already exist before creating new ones 4. **Implement idempotent creation**: Use a get-or-create pattern in your code Here's how to list existing roles: ```bash theme={null} curl -X POST https://api.unkey.com/v2/permissions.listRoles \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{}' ``` Or implement a get-or-create pattern in your code: ```javascript theme={null} // Pseudocode for get-or-create pattern async function getOrCreateRole(name, description) { try { // Try to create the role return await createRole(name, description); } catch (error) { // If it already exists (409 error), get it instead if (error.status === 409) { // Extract the role name from the error message and get it const roles = await listRoles(); return roles.find((r) => r.name.toLowerCase() === name.toLowerCase()); } // Otherwise, rethrow the error throw error; } } ``` ## Common Mistakes * **Not checking for existing roles**: Failing to check if a role already exists before creating it * **Case sensitivity**: Role names are case-insensitive - "Admin" and "admin" are the same * **Retry loops**: Repeatedly trying to create the same role after a failure * **Cross-environment duplication**: Using the same role names across development and production without proper namespacing ## Related Errors * [err:unkey:data:role\_not\_found](./role_not_found) - When the requested role doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to manage roles # role_not_found Source: https://unkey.com/docs/errors/unkey/data/role_not_found The requested role was not found err:unkey:data:role\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested role could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/role_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a role that doesn't exist in the Unkey system. Roles in Unkey are collections of permissions that can be assigned to users or API keys. Common scenarios that trigger this error: * Using an incorrect role ID or name * Referencing a role that has been deleted * Trying to assign a role that doesn't exist in the current workspace * Typos in role names when using name-based lookups ## How To Fix Verify that you're using the correct role ID or name and that the role still exists in your workspace: 1. List all roles in your workspace to find the correct ID 2. Check if the role has been deleted and recreate it if necessary 3. Verify you're working in the correct workspace Here's how to list all roles in your workspace: ```bash theme={null} curl -X POST https://api.unkey.com/v2/permissions.listRoles \ -H "Authorization: Bearer unkey_YOUR_API_KEY" ``` If you need to create a new role, use the appropriate API endpoint: ```bash theme={null} curl -X POST https://api.unkey.com/v2/permissions.createRole \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_YOUR_API_KEY" \ -d '{ "name": "API Reader", "description": "Can read API information" }' ``` ## Common Mistakes * **Incorrect identifiers**: Using wrong role IDs or names * **Deleted roles**: Referencing roles that have been removed * **Case sensitivity**: Role names might be case-sensitive * **Workspace boundaries**: Trying to use roles from another workspace ## Related Errors * [err:unkey:data:permission\_not\_found](./permission_not_found) - When the requested permission doesn't exist * [err:unkey:data:api\_not\_found](./api_not_found) - When the requested API doesn't exist * [err:unkey:authorization:insufficient\_permissions](../authorization/insufficient_permissions) - When you don't have permission to perform operations on roles # workspace_not_found Source: https://unkey.com/docs/errors/unkey/data/workspace_not_found The requested workspace was not found err:unkey:data:workspace\_not\_found ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "The requested workspace could not be found", "status": 404, "title": "Not Found", "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/data/workspace_not_found" } } ``` ## What Happened? This error occurs when you're trying to perform an operation on a workspace that doesn't exist in the Unkey system. This can happen when referencing a workspace by ID or name in API calls. Common scenarios that trigger this error: * Using an incorrect workspace ID * Referencing a workspace that has been deleted * Attempting to access a workspace you don't have permission to see * Typos in workspace names when using name-based lookups ## How To Fix Verify that you're using the correct workspace ID or name and that the workspace still exists: 1. Check your Unkey dashboard to see a list of workspaces you have access to 2. Verify the workspace ID or name in your API calls 3. Ensure you have permission to access the workspace 4. If needed, create a new workspace through the dashboard or API \` ## Common Mistakes * **Deleted workspaces**: Attempting to reference workspaces that have been deleted * **Copy-paste errors**: Using incorrect IDs from documentation examples * **Permission issues**: Trying to access workspaces you've been removed from * **Case sensitivity**: Using incorrect casing in workspace name lookups ## Related Errors * [err:unkey:authorization:workspace\_disabled](../authorization/workspace_disabled) - When the workspace exists but is disabled * [err:unkey:data:api\_not\_found](./api_not_found) - When the requested API doesn't exist * [err:unkey:data:key\_not\_found](./key_not_found) - When the requested key doesn't exist # client_closed_request Source: https://unkey.com/docs/errors/user/bad_request/client_closed_request Client cancelled the request before the server could complete processing `err:user:bad_request:client_closed_request` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Client closed request", "status": 499, "title": "Client Closed Request", "type": "https://unkey.com/docs/errors/user/bad_request/client_closed_request", "errors": [] } } ``` ## What Happened? Your client cancelled the request before our server finished processing it. This happens when: * Your client timeout is shorter than the processing time * Network connection was lost during the request * Your application cancelled the request (user navigated away, etc.) * Load balancer or proxy terminated the connection **HTTP Status 499** is a non-standard but widely used status code that means "Client Closed Request". It was originally defined by nginx and is now used by many web servers to indicate when a client disconnects before receiving the full response. ## Common Causes ### 1. Client Timeout Too Short Your HTTP client may have a timeout that's shorter than our processing time. Make sure your client timeout allows enough time for the operation to complete (typically 30+ seconds). ### 2. Command Line Tools Tools like `curl` or `timeout` commands may have short default timeouts. Use `--max-time 30` with curl or longer timeout values with other tools. ### 3. User Interface Cancellations In web applications, users might navigate away or close tabs before requests complete. This is normal user behavior and should be handled gracefully in your application. ## How to Fix It ### 1. Increase Client Timeouts Make sure your client timeout is reasonable for the operation (typically 30+ seconds). Check your HTTP client documentation for timeout configuration options. ### 2. Handle Network Issues If you're experiencing network connectivity problems that cause your client to disconnect, check your network stability and consider increasing timeout values. ### 3. Check Your Infrastructure If you're using proxies, load balancers, or CDNs: * Verify their timeout settings * Check if they're terminating long-running requests * Ensure they're configured to handle the expected request duration ## When You See This Error ### It's Usually Not Our Fault Status 499 means your client cancelled the request, so the issue is typically: * Your client timeout settings * Network connectivity problems * User behavior (closing browser, etc.) ### Check Your Logs Look for patterns: * Is it happening at specific times? * Are certain operations more affected? * Are there network errors in your client logs? ### You Won't See 499 Errors in Your Client **Important:** Since the client cancels the request before getting a response, your application typically won't receive a 499 status code. Instead, you'll see: * Network timeout errors * Request cancelled/aborted errors * Connection reset errors You can only see 499 errors in server logs, not in your client application. This is why this error mainly helps server operators understand why requests failed. ## Difference from Other Errors * **408 Request Timeout**: Server took too long (server-side issue) * **499 Client Closed Request**: Client cancelled early (client-side issue) * **504 Sentinel Timeout**: Upstream service timeout (infrastructure issue) If you're seeing mostly 499 errors, focus on your client configuration and network connectivity rather than server performance. # invalid_analytics_function Source: https://unkey.com/docs/errors/user/bad_request/invalid_analytics_function Your query uses a function that isn't allowed for security reasons. `err:user:bad_request:invalid_analytics_function` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Function 'file' is not allowed", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/invalid_analytics_function" } } ``` ## What Happened? Your query tried to use a function that's blocked for security reasons! For security, only safe functions are allowed in analytics queries. Functions that could: * Read files from the server (`file`, `executable`) * Make network requests (`url`, `remote`) * Access external systems (`mysql`, `postgresql`, `s3`, `hdfs`) * Modify data or system state ...are all blocked. ## How to Fix It ### 1. Use Allowed Functions Stick to standard analytics functions: ```sql Wrong - Blocked function theme={null} SELECT file('/etc/passwd') FROM key_verifications_v1 ``` ```sql Correct - Safe analytics functions theme={null} SELECT toStartOfHour(time) as hour, COUNT(*) as total, AVG(response_time) as avg_response FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY hour ``` ### 2. Common Safe Functions These are examples of allowed functions: **Aggregate functions:** * `COUNT()`, `SUM()`, `AVG()`, `MIN()`, `MAX()` * `uniq()`, `groupArray()` **Date/time functions:** * `now()`, `today()`, `yesterday()` * `toStartOfHour()`, `toStartOfDay()`, `toStartOfWeek()` * `toDate()`, `toDateTime()` **String functions:** * `concat()`, `substring()`, `lower()`, `upper()` * `length()`, `position()` **Mathematical functions:** * `round()`, `floor()`, `ceil()` * `abs()`, `sqrt()`, `pow()` **Conditional functions:** * `if()`, `multiIf()` * `CASE WHEN ... THEN ... END` ### 3. Remove Dangerous Functions ```sql Blocked - File access theme={null} SELECT file('/path/to/file') FROM key_verifications_v1 ``` ```sql Blocked - Network access theme={null} SELECT * FROM url('http://example.com/data') ``` ```sql Blocked - External DB theme={null} SELECT * FROM mysql('host:port', 'db', 'table', 'user', 'pass') ``` ```sql Safe Alternative - Use only your analytics data theme={null} SELECT api_id, COUNT(*) as verifications FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 DAY GROUP BY api_id ``` ## Commonly Blocked Functions These functions are blocked for security: | Function | Why Blocked | | -------------------------------------- | ------------------------ | | `file()`, `executable()` | File system access | | `url()`, `remote()` | Network requests | | `mysql()`, `postgresql()`, `mongodb()` | External database access | | `s3()`, `hdfs()`, `azureBlobStorage()` | External storage access | | `dictGet()`, `dictGetOrDefault()` | Dictionary access | Need a specific function that's blocked? [Contact support](mailto:support@unkey.com) to discuss your use case - we may be able to safely enable it! # invalid_analytics_query Source: https://unkey.com/docs/errors/user/bad_request/invalid_analytics_query Your SQL query has a syntax error. `err:user:bad_request:invalid_analytics_query` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Syntax error: Expected identifier, got 'FROM' at position 15", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/invalid_analytics_query" } } ``` ## What Happened? Your SQL query has a syntax error! The query parser found invalid SQL syntax that prevents it from being executed. Common causes include: * Missing or extra commas * Unclosed quotes or parentheses * Typos in SQL keywords * Invalid column or table names ## How to Fix It ### 1. Check for Missing Commas ```sql Wrong - Missing comma theme={null} SELECT key_space_id COUNT(*) as total FROM key_verifications_v1 ``` ```sql Correct theme={null} SELECT key_space_id, COUNT(*) as total FROM key_verifications_v1 ``` ### 2. Match Quotes and Parentheses ```sql Wrong - Unclosed quote theme={null} SELECT key_space_id, outcome FROM key_verifications_v1 WHERE key_space_id = 'ks_123 ``` ```sql Correct theme={null} SELECT key_space_id, outcome FROM key_verifications_v1 WHERE key_space_id = 'ks_123' ``` ### 3. Use Correct SQL Keywords ```sql Wrong - Typo in SELECT theme={null} SELCT time, outcome FROM key_verifications_v1 ``` ```sql Correct theme={null} SELECT time, outcome FROM key_verifications_v1 ``` ### 4. Verify Column Names Make sure you're using valid column names from your analytics tables: ```sql theme={null} -- ✓ Valid columns SELECT time, key_space_id, outcome, key_id FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ## Need Help? If you're stuck with a syntax error: 1. **Check the error message** - It usually tells you exactly where the problem is 2. **Test incrementally** - Start with a simple query like `SELECT time, outcome FROM table_name LIMIT 10` and add complexity step by step 3. **Use a SQL validator** - Many online tools can help spot syntax errors 4. **Check the schema** - Refer to the [Schema Reference](/analytics/schema-reference) for valid column names # invalid_analytics_query_type Source: https://unkey.com/docs/errors/user/bad_request/invalid_analytics_query_type Only SELECT queries are allowed for analytics - you tried to use INSERT, UPDATE, DELETE, or another unsupported operation. `err:user:bad_request:invalid_analytics_query_type` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Only SELECT queries are allowed", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/invalid_analytics_query_type" } } ``` ## What Happened? You tried to run a query that modifies data or isn't a SELECT! Analytics queries are read-only - you can only SELECT data, not modify it. Blocked query types: * `INSERT` - Adding new data * `UPDATE` - Modifying existing data * `DELETE` - Removing data * `DROP` - Deleting tables * `CREATE` - Creating tables * `ALTER` - Modifying table structure * `TRUNCATE` - Clearing tables ## How to Fix It ### 1. Use SELECT Instead Analytics is for querying data, not modifying it: ```sql Wrong - INSERT not allowed theme={null} INSERT INTO key_verifications_v1 VALUES (...) ``` ```sql Wrong - UPDATE not allowed theme={null} UPDATE key_verifications_v1 SET outcome = 'VALID' ``` ```sql Wrong - DELETE not allowed theme={null} DELETE FROM key_verifications_v1 WHERE time < now() - INTERVAL 30 DAY ``` ```sql Correct - SELECT queries only theme={null} SELECT api_id, outcome, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY api_id, outcome ``` ### 2. Query Your Data The analytics endpoint is for analyzing your verification data: ```sql theme={null} -- ✓ Count verifications by outcome SELECT outcome, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 DAY GROUP BY outcome -- ✓ Get hourly verification rates SELECT toStartOfHour(time) as hour, COUNT(*) as verifications FROM key_verifications_v1 WHERE time >= now() - INTERVAL 24 HOUR GROUP BY hour ORDER BY hour -- ✓ Find most active APIs SELECT api_id, COUNT(*) as requests FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY api_id ORDER BY requests DESC LIMIT 10 ``` ### 3. Use the Correct API for Data Modification If you need to modify data, use the appropriate Unkey API endpoints: | What You Want | Use This API | | ------------------ | ------------------------------ | | Create a key | `POST /v2/keys.createKey` | | Update a key | `PATCH /v2/keys.updateKey` | | Delete a key | `POST /v2/keys.deleteKey` | | Modify permissions | `POST /v2/keys.addPermissions` | ## Why Read-Only? Analytics queries are read-only for several reasons: 1. **Data integrity** - Verification history should never be modified 2. **Performance** - Read-only queries can be heavily optimized 3. **Security** - Prevents accidental or malicious data corruption 4. **Audit trail** - Preserves accurate historical records Analytics is for understanding your data, not changing it. Use the main Unkey API for creating, updating, or deleting resources. # invalid_analytics_table Source: https://unkey.com/docs/errors/user/bad_request/invalid_analytics_table Your query references a table that doesn't exist or isn't allowed. `err:user:bad_request:invalid_analytics_table` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Access to table 'system.tables' is not allowed", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/invalid_analytics_table" } } ``` ## What Happened? Your query tried to access a table that either doesn't exist or isn't allowed for security reasons. For security, only specific analytics tables are accessible: * `key_verifications_v1` - Raw key verification events * `key_verifications_per_minute_v1` - Minute-level aggregates * `key_verifications_per_hour_v1` - Hour-level aggregates * `key_verifications_per_day_v1` - Day-level aggregates * `key_verifications_per_month_v1` - Month-level aggregates System tables (like `system.*`) and other database tables are blocked. ## How to Fix It ### 1. Use the Correct Table Name ```sql Wrong - System table theme={null} SELECT * FROM system.tables ``` ```sql Correct - Analytics table theme={null} SELECT * FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ### 2. Fix Typos in Table Names ```sql Wrong - Typo theme={null} SELECT * FROM key_verification WHERE time >= now() - INTERVAL 1 DAY ``` ```sql Correct theme={null} SELECT * FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 DAY ``` ## Available Tables | Table Name | Description | | --------------------------------- | ----------------------- | | `key_verifications_v1` | Raw verification events | | `key_verifications_per_minute_v1` | Minute-level aggregates | | `key_verifications_per_hour_v1` | Hour-level aggregates | | `key_verifications_per_day_v1` | Day-level aggregates | | `key_verifications_per_month_v1` | Month-level aggregates | # missing_required_header Source: https://unkey.com/docs/errors/user/bad_request/missing_required_header MissingRequiredHeader indicates a required HTTP header is missing from the request. `err:user:bad_request:missing_required_header` # permissions_query_syntax_error Source: https://unkey.com/docs/errors/user/bad_request/permissions_query_syntax_error Invalid syntax or characters in verifyKey permissions query `err:user:bad_request:permissions_query_syntax_error` ```json Example theme={null} { "meta": { "requestId": "req_2c9a0jf23l4k567" }, "error": { "detail": "Syntax error in permission query: unexpected token 'AND' at position 15. Expected permission name or opening parenthesis.", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/api-reference/errors-v2/user/bad_request/permissions_query_syntax_error", "errors": [ { "location": "body.permissions", "message": "unexpected token 'AND' at position 15", "fix": "Check your query syntax. AND/OR operators must be between permissions, not at the start or end" } ] } } ``` ## What Happened? This error occurs when the permissions query in your `verifyKey` request contains invalid syntax or characters. This can happen due to: * **Invalid characters** in permission names (lexical errors) * **Incorrect query structure** like missing operands or unmatched parentheses (syntax errors) * **Malformed expressions** that don't follow the expected grammar ## Permissions Query Requirements The `verifyKey` endpoint accepts a permissions query string that must follow these rules: ### Valid Characters The query parser accepts these characters: * **Permissions**: Must follow the permission slug format (alphanumeric, dots, underscores, hyphens) * **Letters**: `a-z`, `A-Z` * **Numbers**: `0-9` * **Dots**: `.` for permission namespacing * **Underscores**: `_` in identifiers * **Hyphens**: `-` in identifiers * **Query operators**: `AND`, `OR` (case insensitive) * **Grouping**: `(` `)` for parentheses * **Whitespace**: Spaces, tabs and new lines for separation (ignored by parser) Everything else is not allowed. ### Query Structure A permissions query can be: 1. **A single permission**: `permission_1` 2. **Multiple permissions with AND**: `permission_1 AND permission_2` 3. **Multiple permissions with OR**: `permission_1 OR permission_2` 4. **Grouped expressions**: `(permission_1 OR permission_2) AND permission_3` **Key rules:** * Permission names must be valid permission slugs (letters, numbers, dots, underscores, hyphens) * Use `AND` when all permissions are required * Use `OR` when any of the permissions is sufficient * Use parentheses `()` to group expressions and control precedence * Operators are case insensitive: `AND`, `AnD`, `and` all work. ## Common Errors and Solutions ### 1. Invalid Characters ```bash theme={null} # ❌ Invalid - contains special characters curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission$1 OR permission@2" }' # ✅ Valid - use underscores or hyphens curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 OR permission_2" }' ``` ### 2. Missing Operands ```bash theme={null} # ❌ Invalid - AND without right operand curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND" }' # ✅ Valid - complete AND expression curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND permission_2" }' ``` ### 3. Unmatched Parentheses ```bash theme={null} # ❌ Invalid - missing closing parenthesis curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "(permission_1 AND permission_2" }' # ✅ Valid - balanced parentheses curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "(permission_1 AND permission_2)" }' ``` ### 4. Empty Parentheses ```bash theme={null} # ❌ Invalid - empty parentheses curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND ()" }' # ✅ Valid - parentheses with content curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND (permission_2 OR permission_3)" }' ``` ### 5. Incorrect Operator Placement ```bash theme={null} # ❌ Invalid - operator at start curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "OR permission_1" }' # ✅ Valid - operators between permissions curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 OR permission_2" }' ``` ## Valid Query Examples ### Simple Permission ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1" }' ``` ### AND Operation ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 AND permission_2" }' ``` ### OR Operation ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "permission_1 OR permission_2" }' ``` ### Complex Expressions ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "(permission_1 OR permission_2) AND permission_3" }' ``` ### Nested Expressions ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "key": "sk_123", "permissions": "((permission_1 OR permission_2) AND permission_3) OR permission_4" }' ``` ## Valid Permission Formats ### Simple Names * `permission_1` * `user_read` * `admin-access` ### Namespaced Permissions * `api.users.read` * `billing.invoices.create` * `workspace.settings.update` ### Mixed Formats * `user_management.create` * `billing-service.view` * `service123.feature_a.read` ## Context This error is specific to the `verifyKey` endpoint's permissions query parsing. The query is validated at the application level to ensure it conforms to the expected permission query language syntax. Basic validation like empty strings and length limits are handled at the OpenAPI level before reaching this parser. # query_range_exceeds_retention Source: https://unkey.com/docs/errors/user/bad_request/query_range_exceeds_retention QueryRangeExceedsRetention indicates the query attempts to access data older than the workspace's retention period. `err:user:bad_request:query_range_exceeds_retention` ## What does this error mean? This error occurs when your analytics query attempts to access data older than your workspace's configured data retention period. By default, Unkey retains analytics data for **30 days**. **Note**: If you don't provide a time filter in your query, the system automatically adds one to scope your query to the full retention period (e.g., `time >= now() - INTERVAL 30 DAY`). This error only occurs when you explicitly specify a time range that goes beyond the retention period. ## Common causes 1. **Querying historical data beyond retention**: Your query's time filter includes dates older than your retention period ```sql theme={null} -- If your retention is 30 days, this will fail if querying > 30 days ago SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= 1234567890000 ``` ## How to fix 1. **Add a time filter within retention**: Ensure your query only accesses data within your retention period ```sql theme={null} -- Query last 7 days (within 30-day retention) SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` 2. **Adjust your query range**: If you need to analyze trends, query within your available retention window ```sql theme={null} -- Query the full 30-day retention period SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 30 DAY ``` ## Need longer retention? If your use case requires data retention beyond 30 days, please [contact our support team](https://unkey.com/support) to discuss upgrading your retention period. We can configure custom retention periods based on your needs. ## Related * [Analytics documentation](/apis/features/analytics) * [Analytics query examples](/analytics) # request_body_too_large Source: https://unkey.com/docs/errors/user/bad_request/request_body_too_large Request body exceeds the maximum allowed size limit `err:user:bad_request:request_body_too_large` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "The request body exceeds the maximum allowed size of 100 bytes.", "status": 413, "title": "Request Entity Too Large", "type": "https://unkey.com/docs/errors/user/bad_request/request_body_too_large", "errors": [] } } ``` ## What Happened? Your request was too big! We limit how much data you can send in a single API request to keep everything running smoothly. This usually happens when you're trying to send a lot of data at once - like huge metadata objects or really long strings in your request. ## How to Fix It ### 1. Trim Down Your Request The most common cause is putting too much data in the `meta` field or other parts of your request. ```bash Too Big theme={null} curl -X POST https://api.unkey.com/v2/keys.create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "apiId": "api_123", "name": "My Key", "meta": { "userProfile": "... really long user profile data ...", "settings": { /* huge nested object with tons of properties */ } } }' ``` ```bash Just Right theme={null} curl -X POST https://api.unkey.com/v2/keys.create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "apiId": "api_123", "name": "My Key", "meta": { "userId": "user_123", "tier": "premium" } }' ``` ### 2. Store Big Data Elsewhere Instead of cramming everything into your API request: * Store large data in your own database * Only send IDs or references to Unkey * Fetch the full data when you need it ## Need a Higher Limit? **Got a special use case?** If you have a legitimate need to send larger requests, we'd love to hear about it! [Contact our support team](mailto:support@unkey.com) and include: * What you're building * Why you need to send large requests * An example of the data you're trying to send We'll work with you to find a solution that works for your use case. ``` ``` # request_body_unreadable Source: https://unkey.com/docs/errors/user/bad_request/request_body_unreadable Request body could not be read due to malformed request or connection issues `err:user:bad_request:request_body_unreadable` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "The request body could not be read.", "status": 400, "title": "Bad Request", "type": "https://unkey.com/docs/errors/user/bad_request/request_body_unreadable", "errors": [] } } ``` ## What Happened? The server couldn't read your request body. This typically happens when there's an issue with how the request was formed or transmitted. Common causes include: * Malformed HTTP request structure * Network connection closed prematurely during transmission * Invalid or mismatched `Content-Length` header * Data corruption during transmission * Client-side network interruption ## How to Fix It ### 1. Check Your Request Structure Make sure your HTTP request is properly formatted and all required headers are present: ```bash theme={null} curl -X POST https://api.unkey.com/v2/keys.create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer unkey_XXXX" \ -d '{ "apiId": "api_123", "name": "My Key" }' ``` ### 2. Check Network Stability If you're experiencing intermittent failures: * Implement retry logic with exponential backoff * Check your network connection stability * Consider timeout values that may be too aggressive ### 3. Avoid Partial Uploads Ensure you're sending the complete request body in one go, not streaming partial data that might get interrupted. ## Still Having Issues? If you continue to see this error after checking the above: 1. Check your HTTP client library documentation for known issues 2. Try a different HTTP client to isolate the problem 3. [Contact our support team](mailto:support@unkey.com) with: * Your request ID (from the error response) * The HTTP client/library you're using * Any network logs or error messages # request_timeout Source: https://unkey.com/docs/errors/user/bad_request/request_timeout Request took too long to process and exceeded the server timeout `err:user:bad_request:request_timeout` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Request timeout", "status": 408, "title": "Request Timeout", "type": "https://unkey.com/docs/errors/user/bad_request/request_timeout", "errors": [] } } ``` ## What Happened? Your request took too long to process and was automatically terminated by the server. This typically happens when: * Database queries are running slowly * External services are responding slowly * The API is under heavy load * Network connectivity issues are causing delays ## How to Fix It ### 1. Retry the Request Most timeout errors are temporary. Simply retry your request after a short delay with exponential backoff (wait longer after each failed attempt). ### 2. Check Your Network Slow or unstable network connections can cause timeouts: * Test from a different network or location * Check if you're experiencing high latency to our servers ## When This Happens Often If you're seeing frequent timeout errors: ### Check Our Status Page Visit [status.unkey.com](https://status.unkey.com) to see if we're experiencing any service issues. ### Contact Support **Still having issues?** We're here to help! [Contact our support team](mailto:support@unkey.com) and include: * Your request IDs from the error responses * The approximate time the errors occurred * Your typical request patterns and volume We can investigate what might be causing the slowdowns and help optimize your integration. ## Difference from Other Timeout Errors * **408 Request Timeout**: The server took too long to process your request (this error) * **499 Client Closed Request**: Your client cancelled the request before the server finished * **504 Sentinel Timeout**: An upstream service (like a database) timed out If you're seeing 408 errors, the issue is usually on our side or with network connectivity. # query_quota_exceeded Source: https://unkey.com/docs/errors/user/too_many_requests/query_quota_exceeded You've exceeded your workspace's analytics query quota for the current time window. `err:user:too_many_requests:query_quota_exceeded` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Workspace has exceeded the analytics query quota of 1000 queries per hour", "status": 429, "title": "Too Many Requests", "type": "https://unkey.com/docs/errors/user/too_many_requests/query_quota_exceeded" } } ``` ## What Happened? Your workspace has made too many analytics queries in a short period! We limit the number of queries you can run per hour to keep the analytics service fast and reliable for everyone. This is a rate limit on the **number of queries**, not about individual query complexity. ## How to Fix It ### 1. Wait and Retry The quota resets every hour. Wait a bit and try your query again. ### 2. Cache Your Results Instead of running the same query repeatedly, cache the results in your application: ```typescript Bad - Queries on Every Request theme={null} app.get('/dashboard', async (req, res) => { // This runs a query EVERY time someone loads the dashboard const stats = await fetch('https://api.unkey.com/v1/analytics', { method: 'POST', headers: { 'Authorization': 'Bearer unkey_XXX' }, body: JSON.stringify({ query: 'SELECT COUNT(*) FROM key_verifications' }) }) res.json(stats) }) ``` ```typescript Better - Cache for 5 Minutes theme={null} import { Cache } from 'your-cache-library' const cache = new Cache() app.get('/dashboard', async (req, res) => { // Check cache first let stats = cache.get('dashboard-stats') if (!stats) { // Only query if cache is empty stats = await fetch('https://api.unkey.com/v1/analytics', { method: 'POST', headers: { 'Authorization': 'Bearer unkey_XXX' }, body: JSON.stringify({ query: 'SELECT COUNT(*) FROM key_verifications' }) }) // Cache for 5 minutes cache.set('dashboard-stats', stats, { ttl: 300 }) } res.json(stats) }) ``` ### 3. Batch Your Queries If you're making multiple queries, try to combine them into a single query with JOINs or subqueries. ### 4. Use Webhooks Instead For real-time updates, consider using webhooks instead of polling the analytics API repeatedly. ## Default Quota | Plan | Queries per Hour | | ---------- | ---------------- | | Free | 1,000 | | Pro | 10,000 | | Enterprise | Custom | ## Need a Higher Quota? **Running into limits often?** We can increase your quota! [Contact our support team](mailto:support@unkey.com) and tell us: * What you're building * Why you need more queries per hour * Your current usage patterns We'll work with you to find the right quota for your needs, or help optimize your query patterns. # workspace_rate_limited Source: https://unkey.com/docs/errors/user/too_many_requests/workspace_rate_limited Your workspace has exceeded its API rate limit for the current time window. `err:user:too_many_requests:workspace_rate_limited` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "This workspace has exceeded its API rate limit of 100/10s. Please try again later.", "status": 429, "title": "Too Many Requests", "type": "https://unkey.com/docs/errors/user/too_many_requests/workspace_rate_limited" } } ``` ## What Happened? Your workspace has been configured with an API rate limit and you've exceeded it. Most workspaces don't have a rate limit by default — this is only applied when needed. The response includes standard rate limit headers (`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, `Retry-After`) so your client knows when to retry. ## Need Help? If you're hitting this limit unexpectedly, [contact us](mailto:support@unkey.com) and we'll sort it out. # query_execution_timeout Source: https://unkey.com/docs/errors/user/unprocessable_entity/query_execution_timeout Your query took longer than the maximum execution time allowed. `err:user:unprocessable_entity:query_execution_timeout` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Query exceeded the maximum execution time of 30 seconds", "status": 422, "title": "Unprocessable Entity", "type": "https://unkey.com/docs/errors/user/unprocessable_entity/query_execution_timeout" } } ``` ## What Happened? Your analytics query took too long to execute! We limit queries to 30 seconds to keep the analytics service responsive for everyone. This usually happens when you're querying a large time range or complex data without enough filters. ## How to Fix It ### 1. Query Smaller Time Ranges The most common fix is to reduce the time range: ```sql Too Long theme={null} SELECT COUNT(*) FROM key_verifications WHERE time >= now() - INTERVAL 1 YEAR GROUP BY toStartOfDay(time) ``` ```sql Better theme={null} SELECT COUNT(*) FROM key_verifications WHERE time >= now() - INTERVAL 7 DAY GROUP BY toStartOfDay(time) ``` ### 2. Add More Filters Filter your data to reduce the amount of work the query needs to do: ```sql Slow theme={null} SELECT api_id, COUNT(*) as total FROM key_verifications GROUP BY api_id ``` ```sql Faster theme={null} SELECT api_id, COUNT(*) as total FROM key_verifications WHERE time >= now() - INTERVAL 1 DAY AND outcome = 'VALID' GROUP BY api_id ``` ### 3. Use Aggregated Tables For historical data, use pre-aggregated tables instead of raw events: ```sql Slow - Scans millions of raw events theme={null} SELECT toStartOfHour(time) as hour, COUNT(*) as total FROM key_verifications_raw_v2 WHERE time >= now() - INTERVAL 30 DAY GROUP BY hour ``` ```sql Fast - Uses pre-aggregated data theme={null} SELECT time as hour, SUM(count) as total FROM key_verifications_per_hour_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY hour ``` ### 4. Limit Result Size Add a LIMIT clause to stop processing once you have enough data: ```sql theme={null} SELECT api_id, COUNT(*) as total FROM key_verifications WHERE time >= now() - INTERVAL 7 DAY GROUP BY api_id ORDER BY total DESC LIMIT 100 ``` ## Need Longer Execution Time? **Have a legitimate need for longer-running queries?** Contact our support team! [Reach out to support](mailto:support@unkey.com) and tell us: * What you're trying to analyze * Why the query needs more than 30 seconds * An example of the query you're running We'll review your use case and see if we can accommodate your needs. # query_memory_limit_exceeded Source: https://unkey.com/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded Your query used more memory than allowed. `err:user:unprocessable_entity:query_memory_limit_exceeded` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Query exceeded the maximum memory limit of 2GB", "status": 422, "title": "Unprocessable Entity", "type": "https://unkey.com/docs/errors/user/unprocessable_entity/query_memory_limit_exceeded" } } ``` ## What Happened? Your query tried to use more than 2GB of memory! We limit memory usage to keep the analytics service stable and fast for everyone. This typically happens when you're selecting too many rows, using large GROUP BY operations, or performing complex JOINs without enough filtering. ## How to Fix It ### 1. Use Aggregations Instead of Raw Data Instead of fetching all rows, aggregate the data: ```sql Memory Intensive - Fetches all rows theme={null} SELECT * FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ``` ```sql Memory Efficient - Aggregates data theme={null} SELECT toStartOfHour(time) as hour, api_id, COUNT(*) as total, AVG(response_time) as avg_response FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY GROUP BY hour, api_id ``` ### 2. Add More Filters Reduce the amount of data the query needs to process: ```sql Too Much Data theme={null} SELECT api_id, key_id, outcome, time FROM key_verifications_v1 WHERE time >= now() - INTERVAL 30 DAY ``` ```sql Filtered Query theme={null} SELECT api_id, key_id, outcome, time FROM key_verifications_v1 WHERE time >= now() - INTERVAL 1 DAY AND api_id = 'api_123' AND outcome = 'VALID' ``` ### 3. Limit Result Size Add a LIMIT to cap the number of rows: ```sql theme={null} SELECT api_id, key_id, outcome, time FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY ORDER BY time DESC LIMIT 10000 ``` ### 4. Avoid Large GROUP BY Cardinality GROUP BY on high-cardinality columns (like `key_id`) uses a lot of memory. Instead, group by lower-cardinality columns: ```sql High Memory - Millions of unique keys theme={null} SELECT key_id, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY key_id ``` ```sql Lower Memory - Hundreds of APIs theme={null} SELECT api_id, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 30 DAY GROUP BY api_id ``` ## Need More Memory? **Have a legitimate need for higher memory limits?** Contact our support team! [Reach out to support](mailto:support@unkey.com) and tell us: * What you're trying to analyze * Why the query needs more than 2GB of memory * An example of the query you're running We'll review your use case and see if we can accommodate your needs. # query_rows_limit_exceeded Source: https://unkey.com/docs/errors/user/unprocessable_entity/query_rows_limit_exceeded Your query tried to scan more rows than allowed. `err:user:unprocessable_entity:query_rows_limit_exceeded` ```json Example theme={null} { "meta": { "requestId": "req_4dgzrNP3Je5mU1tD" }, "error": { "detail": "Query exceeded the maximum rows to scan limit of 100 million", "status": 422, "title": "Unprocessable Entity", "type": "https://unkey.com/docs/errors/user/unprocessable_entity/query_rows_limit_exceeded" } } ``` ## What Happened? Your query tried to scan more than 100 million rows! We limit the number of rows that can be scanned to keep queries fast and prevent resource exhaustion. This happens when you query large time ranges or don't filter your data enough, causing ClickHouse to scan millions of rows even if the final result is small. ## How to Fix It ### 1. Add Time Range Filters Always filter by time to limit the number of rows scanned: ```sql Scans Too Many Rows theme={null} SELECT COUNT(*) FROM key_verifications_v1 WHERE outcome = 'VALID' ``` ```sql Limited Scan theme={null} SELECT COUNT(*) FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY AND outcome = 'VALID' ``` ### 2. Use More Selective Filters Add filters that reduce the data before aggregation: ```sql Scans Everything theme={null} SELECT api_id, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 90 DAY GROUP BY api_id ``` ```sql Scans Less theme={null} SELECT api_id, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 7 DAY AND api_id IN ('api_123', 'api_456') GROUP BY api_id ``` ### 3. Use Pre-Aggregated Tables For historical queries, use aggregated tables that have fewer rows: ```sql Raw Table - 100M+ rows theme={null} SELECT toStartOfDay(time) as day, COUNT(*) as total FROM key_verifications_v1 WHERE time >= now() - INTERVAL 90 DAY GROUP BY day ``` ```sql Aggregated Table - 2K rows theme={null} SELECT time as day, SUM(count) as total FROM key_verifications_v1_per_day_v1 WHERE time >= now() - INTERVAL 90 DAY GROUP BY day ``` ### 4. Query in Smaller Batches Instead of one large query, break it into smaller time windows: ```javascript theme={null} // Instead of querying 90 days at once const results = []; for (let i = 0; i < 90; i += 7) { const start = `now() - INTERVAL ${i + 7} DAY`; const end = `now() - INTERVAL ${i} DAY`; const result = await query(` SELECT COUNT(*) as total FROM key_verifications_v1 WHERE time >= ${start} AND time < ${end} `); results.push(result); } ``` ## Need Higher Row Limits? **Have a legitimate need to scan more rows?** Contact our support team! [Reach out to support](mailto:support@unkey.com) and tell us: * What you're trying to analyze * Why you need to scan more than 100 million rows * An example of the query you're running We'll review your use case and help optimize your query or adjust limits if needed. # Glossary Source: https://unkey.com/docs/glossary Key terms and concepts used throughout Unkey ## Core Concepts ### Workspace A workspace is your top-level account container. Everything in Unkey belongs to a workspace. * Can be personal or team-based * Contains APIs, keys, identities, rate limit namespaces * One owner who can invite members and assign roles * Billing is per-workspace ### API An API is a container for related API keys. Use APIs to organize keys by: * **Product**: `payments-api`, `analytics-api` * **Environment**: `production`, `staging`, `development` * **Tier**: `free-tier`, `pro-tier`, `enterprise` Each key belongs to exactly one API. ### API Key The credential your users include in requests to authenticate with your API. Unkey manages the full lifecycle: * **Creation**: Generate with custom prefix, expiration, limits * **Verification**: Validate and check permissions in \~30ms globally * **Revocation**: Disable or delete when needed Example: `sk_live_abc123...` ### Root Key A special key that grants access to Unkey's management API. Use root keys to: * Create, update, delete API keys * Manage rate limits and permissions * Access analytics Root keys have full access to your workspace. Never expose them in client-side code or public repositories. ## Key Features ### Identity A way to link multiple keys to a single user or entity. Identities let you: * Track usage across all of a user's keys * Share rate limits between keys * Store user metadata once, not per-key Use `externalId` to link to your own user IDs: `externalId: "user_123"` ### Credits (Remaining) Limit how many times a key can be used. Different from rate limiting: | Feature | Credits | Rate Limits | | -------- | ----------------------------- | --------------------- | | Resets? | No (unless refill configured) | Yes, every window | | Use case | Total quota | Requests per time | | Example | "1000 requests total" | "100 requests/minute" | Credits decrement with each verification. When they hit 0, the key is invalid. ### Refill Automatically restore credits on a schedule: * **Daily**: Reset at midnight UTC * **Monthly**: Reset on a specific day Example: "10,000 credits, refills on the 1st of each month" ### Rate Limit Restrict requests within a time window. Prevents abuse and ensures fair usage. * **Limit**: Maximum requests allowed * **Duration**: Time window (e.g., 60 seconds) * **Namespace**: Grouping for related limits Example: "100 requests per minute" ### Rate Limit Namespace A container for related rate limits. Namespaces let you: * Apply limits across keys or identities * Set overrides for specific identifiers * Track analytics per namespace Example namespaces: `api-requests`, `auth-attempts`, `file-uploads` ### Override Custom rate limit for a specific identifier. Overrides let you: * Give VIP users higher limits * Throttle suspicious users * Test with elevated limits Configured in the dashboard — no code changes needed. ## Permissions & Authorization ### Permission A specific action a key can perform. Examples: * `documents.read` * `documents.write` * `billing.manage` Permissions are checked during verification. ### Role A named collection of permissions. Examples: * `admin` → all permissions * `editor` → read + write permissions * `viewer` → read-only permissions Attach roles to keys instead of individual permissions for easier management. ## Configuration ### Prefix A string prepended to generated keys for identification: * `sk_live_` → production key * `sk_test_` → test environment key * `pk_` → publishable key Prefixes help users identify key types at a glance. ### Metadata Custom JSON data attached to a key. Store anything relevant: ```json theme={null} { "environment": "production", "plan": "enterprise", "customerId": "cust_123" } ``` Metadata is returned during verification. ### External ID Your identifier for a user or entity. Links Unkey identities to your system: * `user_123` (your user ID) * `org_456` (your org ID) * `team_789` (your team ID) ## Verification Responses ### Valid Key exists and passed all checks (not expired, not disabled, has credits, not rate limited, has required permissions). ### Code The reason a verification succeeded or failed: | Code | Meaning | | -------------------------- | --------------------------- | | `VALID` | Key is valid | | `NOT_FOUND` | Key doesn't exist | | `EXPIRED` | Key past expiration date | | `DISABLED` | Key manually disabled | | `USAGE_EXCEEDED` | No credits remaining | | `RATE_LIMITED` | Rate limit exceeded | | `INSUFFICIENT_PERMISSIONS` | Missing required permission | | `FORBIDDEN` | IP whitelist violation | ## Analytics ### Verification A single key validation request. Each verification is logged with: * Timestamp * Key ID * Result (valid/invalid + code) * Tags (custom dimensions you add) * Geographic origin ### Tags Custom strings attached to verifications for analytics: ``` path=/v1/users method=POST version=2024-01 ``` Use tags to segment analytics by endpoint, feature, version, etc. # What is Unkey? Source: https://unkey.com/docs/introduction The API development platform for modern teams Unkey is the API development platform that handles the infrastructure you'd rather not build yourself: **API key management**, **rate limiting**, and **usage tracking** — all with global performance and zero servers to manage. Issue, verify, and revoke keys with metadata, expiration, and usage limits built in. Protect any endpoint from abuse — standalone or attached to API keys. Know who's using your API, how much, and when — with analytics out of the box. ## Why Unkey? Building API infrastructure from scratch means managing databases, Redis clusters, edge caching, and analytics pipelines. Unkey handles all of that so you can focus on your actual product. | DIY Approach | With Unkey | | ------------------------------------- | ------------------------- | | Set up key storage, hashing, rotation | `unkey.keys.create()` | | Deploy Redis for rate limiting | `unkey.ratelimit.limit()` | | Build usage tracking & dashboards | Built-in analytics | | Manage infrastructure globally | Globally distributed | Unkey never stores your API keys in plain text. We hash them before storage, so even if our database were compromised, your keys stay safe. ## What can you build? Issue API keys to your users, verify them on every request, and automatically reject invalid or expired keys. ```ts theme={null} const { meta, data } = await unkey.keys.verify({ key: "sk_123..." }); if (!data.valid) return new Response("Unauthorized", { status: 401 }); ``` Limit requests per user, per IP, per endpoint — or any identifier you choose. No Redis required. ```ts theme={null} const { success } = await limiter.limit(userId); if (!success) return new Response("Too many requests", { status: 429 }); ``` Track API usage per customer with credit limits and automatic refills. Perfect for tiered pricing. ```ts theme={null} // Create a key with 1000 credits/month that refills automatically try { const { meta, data } = await unkey.keys.create({ apiId: "api_...", credits: { remaining: 1000, refill: { amount: 1000, refillDay: 1 } } }); } catch (err) { console.error(err); throw err; } ``` Use Identities to group keys under users or organizations, sharing rate limits across all their keys. ```ts theme={null} // All keys for this identity share the same rate limit pool try { const { meta, data } = await unkey.keys.create({ apiId: "api_...", externalId: "org_acme", ratelimits: [{ name: "requests", limit: 10000, duration: 3600000 }] }); } catch (err) { console.error(err); throw err; } ``` ## Get started Create your first API and verify a key in under 5 minutes. Step-by-step guides for Next.js, Bun, Express, Hono, and more. ## Choose your path Issue keys to users, verify on requests, track usage per key. Protect endpoints from abuse without managing Redis infrastructure. Start with API keys — rate limiting can be added per-key or standalone. Bring existing keys to Unkey without disrupting your users. ## Join the community Questions? Ideas? Just want to hang out with other API builders? Chat with the team and community Star us, report issues, contribute Follow for updates # unkey-go Source: https://unkey.com/docs/libraries/go/api Unkey's API provides programmatic access for all resources within our platform. ## SDK Installation To add the SDK as a dependency to your project: ```bash theme={null} go get github.com/unkeyed/sdks/api/go/v2@latest ``` ## SDK Example Usage ### Example ```go theme={null} package main import ( "context" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" "log" "os" ) func main() { ctx := context.Background() s := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) res, err := s.Apis.CreateAPI(ctx, components.V2ApisCreateAPIRequestBody{ Name: "payment-service-production", }) if err != nil { log.Fatal(err) } if res.V2ApisCreateAPIResponseBody != nil { // handle response } } ``` ## Repository The full autogenerated documentation can be found on GitHub. # Overview Source: https://unkey.com/docs/libraries/go/overview Complete guide to using Unkey in Go applications The `unkey-go` SDK provides full access to Unkey's API for managing keys, verifying requests, and rate limiting. ## Installation ```bash theme={null} go get github.com/unkeyed/sdks/api/go/v2@latest ``` ## Quick Start ### Initialize the client ```go theme={null} package main import ( "os" unkey "github.com/unkeyed/sdks/api/go/v2" ) func main() { client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) // Use client... } ``` Never expose your root key in client-side code or commit it to version control. *** ## Verify an API Key Check if a user's API key is valid: ```go theme={null} package main import ( "context" "fmt" "log" "os" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) func main() { ctx := context.Background() client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) apiKey := "sk_live_..." // From request header res, err := client.Keys.VerifyKey(ctx, components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { log.Fatalf("Verification failed: %v", err) } result := res.V2KeysVerifyKeyResponseBody if !result.Valid { fmt.Printf("Key invalid: %s\n", *result.Code) return } fmt.Println("Key is valid!") if result.OwnerID != nil { fmt.Printf("Owner: %s\n", *result.OwnerID) } if result.Credits != nil { fmt.Printf("Credits remaining: %d\n", *result.Credits) } } ``` ### HTTP middleware example ```go theme={null} package main import ( "context" "net/http" "os" "strings" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // AuthMiddleware verifies API keys on incoming requests func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Extract API key from header authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, "Missing Authorization header", http.StatusUnauthorized) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") // Verify with Unkey res, err := unkeyClient.Keys.VerifyKey(r.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { http.Error(w, "Verification service unavailable", http.StatusServiceUnavailable) return } if !res.V2KeysVerifyKeyResponseBody.Valid { http.Error(w, "Invalid API key", http.StatusUnauthorized) return } // Add user info to context if needed ctx := context.WithValue(r.Context(), "keyId", res.V2KeysVerifyKeyResponseBody.KeyID) next.ServeHTTP(w, r.WithContext(ctx)) }) } func main() { mux := http.NewServeMux() mux.HandleFunc("/api/protected", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Access granted!")) }) // Wrap with auth middleware http.ListenAndServe(":8080", AuthMiddleware(mux)) } ``` ### Gin middleware example ```go theme={null} package main import ( "net/http" "os" "strings" "github.com/gin-gonic/gin" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } func UnkeyAuth() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing API key"}) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") res, err := unkeyClient.Keys.VerifyKey(c.Request.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": "Verification failed"}) return } if !res.V2KeysVerifyKeyResponseBody.Valid { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Invalid API key", "code": res.V2KeysVerifyKeyResponseBody.Code, }) return } // Store verification result in context c.Set("unkeyResult", res.V2KeysVerifyKeyResponseBody) c.Next() } } func main() { r := gin.Default() // Protected routes api := r.Group("/api", UnkeyAuth()) { api.GET("/data", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Access granted"}) }) } r.Run(":8080") } ``` *** ## Create API Keys Issue new keys for your users: ```go theme={null} package main import ( "context" "fmt" "log" "os" "time" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) func main() { ctx := context.Background() client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) // Optional values prefix := "sk_live" externalID := "user_123" name := "Production key" expires := time.Now().Add(30 * 24 * time.Hour).UnixMilli() remaining := int64(1000) res, err := client.Keys.CreateKey(ctx, components.V2KeysCreateKeyRequestBody{ APIID: "api_...", Prefix: &prefix, ExternalID: &externalID, Name: &name, Expires: &expires, Credits: &components.KeyCreditsData{ Remaining: &remaining, }, Meta: map[string]any{ "plan": "pro", }, }) if err != nil { log.Fatalf("Failed to create key: %v", err) } result := res.V2KeysCreateKeyResponseBody // Send this to your user - only time you'll see the full key! fmt.Printf("New key: %s\n", result.Key) fmt.Printf("Key ID: %s\n", result.KeyID) } ``` The full API key is only returned once at creation. Unkey stores only a hash. *** ## Update Keys Modify an existing key: ```go theme={null} enabled := true name := "Updated name" _, err := client.Keys.UpdateKey(ctx, components.V2KeysUpdateKeyRequestBody{ KeyID: "key_...", Name: &name, Enabled: &enabled, Meta: map[string]any{ "plan": "enterprise", }, }) if err != nil { log.Fatalf("Failed to update key: %v", err) } ``` *** ## Delete Keys Permanently revoke a key: ```go theme={null} _, err := client.Keys.DeleteKey(ctx, components.V2KeysDeleteKeyRequestBody{ KeyID: "key_...", }) if err != nil { log.Fatalf("Failed to delete key: %v", err) } ``` *** ## Rate Limiting Use the rate limit API directly: ```go theme={null} res, err := client.Ratelimits.Limit(ctx, components.V2RatelimitsLimitRequestBody{ Namespace: "my-app", Identifier: "user_123", Limit: 100, Duration: 60000, // 60 seconds }) if err != nil { log.Fatalf("Rate limit check failed: %v", err) } if !res.V2RatelimitsLimitResponseBody.Success { fmt.Printf("Rate limited. Reset at: %d\n", res.V2RatelimitsLimitResponseBody.Reset) } else { fmt.Printf("Allowed. %d requests remaining\n", res.V2RatelimitsLimitResponseBody.Remaining) } ``` *** ## Error Handling ```go theme={null} res, err := client.Keys.CreateKey(ctx, components.V2KeysCreateKeyRequestBody{ APIID: "api_...", }) if err != nil { // Check for specific error types var apiErr *components.APIError if errors.As(err, &apiErr) { fmt.Printf("API Error: %s - %s\n", apiErr.Code, apiErr.Message) } else { fmt.Printf("Unexpected error: %v\n", err) } return } // Safe to use result fmt.Printf("Created key: %s\n", res.V2KeysCreateKeyResponseBody.Key) ``` *** ## Full Reference Complete auto-generated API reference # Nuxt Module Source: https://unkey.com/docs/libraries/nuxt/overview Nuxt module for Unkey If you are using Nuxt, you can benefit from an almost zero-config experience with the `@unkey/nuxt` module. ## Install ```bash npm theme={null} npm install @unkey/nuxt ``` ```bash pnpm theme={null} pnpm add @unkey/nuxt ``` ```bash yarn theme={null} yarn add @unkey/nuxt ``` ```bash bun theme={null} bun install @unkey/nuxt ``` ## Configuration `@unkey/nuxt` just requires your root key. Create an `.env` file in your project and add the following: ```env theme={null} NUXT_UNKEY_TOKEN= ``` This can also be configured at runtime by setting the `NUXT_UNKEY_TOKEN` environment variable. From this point onward, `@unkey/nuxt` will automatically: 1. verify any API requests with an `Authorization: Bearer xxx` header. 2. register a `useUnkey()` helper that allows access to an automatically configured unkey instance. ## Usage ### Automatic verification You can access the automatically-verified `unkey` context on the server with `event.context.unkey` in your server routes or `useRequestEvent().context.unkey` in the Vue part of your app. For example: ```ts theme={null} export default defineEventHandler(async (event) => { if (!event.context.unkey.valid) { throw createError({ statusCode: 403, message: "Invalid API key" }) } // return authorised information return { // ... }; }); ``` ```html theme={null} ``` ## Unkey helper For more about how to use the configured helper provided by `useUnkey()`, you can see the API docs for [the TypeScript client](/libraries/ts/api). For example: ```ts theme={null} const unkey = useUnkey(); try { const { meta, data } = await unkey.keys.create({ apiId: "api_7oKUUscTZy22jmVf9THxDA", prefix: "xyz", byteLength: 16, externalId: "user_123", // Link to your user meta: { hello: "world", }, expires: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days from now ratelimit: { limit: 10, duration: 1000, // 10 requests per second }, }); console.log(data.key); } catch (err) { console.error(err); throw err; } ``` ### Disable telemetry By default, Unkey collects anonymous telemetry data to help us understand how our SDKs are used. If you wish to disable this, you can do so by passing a boolean flag to the constructor: ```ts theme={null} const unkey = useUnkey({ disableTelemetry: true }) ``` # unkey.py Source: https://unkey.com/docs/libraries/py/api Unkey's API provides programmatic access for all resources within our platform. ## SDK Installation > \[!NOTE] > **Python version upgrade policy** > > Once a Python version reaches its [official end of life date](https://devguide.python.org/versions/), a 3-month grace period is provided for users to upgrade. Following this grace period, the minimum python version supported in the SDK will be updated. The SDK can be installed with either *pip* or *poetry* package managers. ### PIP *PIP* is the default package installer for Python, enabling easy installation and management of packages from PyPI via the command line. ```bash theme={null} pip install unkey.py ``` ### Poetry *Poetry* is a modern tool that simplifies dependency management and package publishing by using a single `pyproject.toml` file to handle project metadata and dependencies. ```bash theme={null} poetry add unkey.py ``` ### Shell and script usage with `uv` You can use this SDK in a Python shell with [uv](https://docs.astral.sh/uv/) and the `uvx` command that comes with it like so: ```shell theme={null} uvx --from unkey.py python ``` It's also possible to write a standalone Python script without needing to set up a whole project like so: ```python theme={null} #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.9" # dependencies = [ # "unkey.py", # ] # /// from unkey.py import Unkey sdk = Unkey( # SDK arguments ) # Rest of script here... ``` Once that is saved to a file, you can run it with `uv run script.py` where `script.py` can be replaced with the actual file name. ## SDK Example Usage ### Example ```python theme={null} # Synchronous Example from unkey.py import Unkey with Unkey( root_key="", ) as unkey: res = unkey.apis.create_api(name="payment-service-production") # Handle response print(res) ``` The same SDK client can also be used to make asynchronous requests by importing asyncio. ```python theme={null} # Asynchronous Example import asyncio from unkey.py import Unkey async def main(): async with Unkey( root_key="", ) as unkey: res = await unkey.apis.create_api_async(name="payment-service-production") # Handle response print(res) asyncio.run(main()) ``` ## Repository The full autogenerated documentation can be found on GitHub. # Overview Source: https://unkey.com/docs/libraries/py/overview Complete guide to using Unkey in Python applications The `unkey.py` SDK provides full access to Unkey's API for managing keys, verifying requests, and rate limiting. ## Installation ```bash pip theme={null} pip install unkey.py ``` ```bash poetry theme={null} poetry add unkey.py ``` ```bash uv theme={null} uv add unkey.py ``` **Requirements:** Python 3.9+ ## Quick Start ### Initialize the client ```python theme={null} import os from unkey import Unkey unkey = Unkey(root_key="unkey_...") # Or use environment variable unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) ``` Never expose your root key in client-side code or commit it to version control. *** ## Verify an API Key Check if a user's API key is valid: ```python theme={null} from typing import Optional from unkey import Unkey, ApiError unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) def verify_request(api_key: str) -> Optional[dict]: """Verify an API key and return the result or None if invalid.""" try: result = unkey.keys.verify_key(key=api_key) if not result.valid: print(f"Key invalid: {result.code}") return None return { "valid": True, "key_id": result.key_id, "meta": result.meta, "remaining": result.credits, } except ApiError as e: print(f"Unkey error: {e.message}") return None ``` ### Verification response fields | Field | Type | Description | | ------------- | ------------- | ------------------------------------------------------------------ | | `valid` | `bool` | Whether the key passed all checks | | `code` | `str` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `key_id` | `str` | The key's unique identifier | | `name` | `str?` | Human-readable name of the key | | `meta` | `dict?` | Custom metadata associated with the key | | `expires` | `int?` | Unix timestamp (in milliseconds) when the key will expire (if set) | | `credits` | `int?` | Remaining uses (if usage limits set) | | `enabled` | `bool` | Whether the key is enabled | | `roles` | `list[str]?` | Roles attached to the key | | `permissions` | `list[str]?` | Permissions attached to the key | | `identity` | `dict?` | Identity info if `external_id` was set when creating the key | | `ratelimits` | `list[dict]?` | Rate limit states (if rate limiting configured) | ### FastAPI example ```python theme={null} from fastapi import FastAPI, Header, HTTPException from unkey import Unkey import os app = FastAPI() unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) @app.get("/api/protected") async def protected_route(x_api_key: str = Header(...)): result = unkey.keys.verify_key(key=x_api_key) if not result.valid: raise HTTPException( status_code=401 if result.code == "NOT_FOUND" else 403, detail=f"Unauthorized: {result.code}" ) return { "message": "Access granted", "key_id": result.key_id, "remaining_credits": result.credits } ``` ### Flask example ```python theme={null} from flask import Flask, request, jsonify from unkey import Unkey import os app = Flask(__name__) unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) @app.route("/api/protected") def protected_route(): api_key = request.headers.get("X-API-Key") if not api_key: return jsonify({"error": "Missing API key"}), 401 result = unkey.keys.verify_key(key=api_key) if not result.valid: return jsonify({"error": result.code}), 401 return jsonify({ "message": "Access granted", "key_id": result.key_id }) ``` *** ## Create API Keys Issue new keys for your users: ```python theme={null} from datetime import datetime, timedelta result = unkey.keys.create_key( api_id="api_...", # Optional but recommended prefix="sk_live", external_id="user_123", # Link to your user name="Production key", # Optional: Expiration expires=(datetime.now() + timedelta(days=30)).timestamp() * 1000, # Optional: Usage limits remaining=1000, refill={ "amount": 1000, "interval": "monthly", }, # Optional: Rate limits ratelimit={ "limit": 100, "duration": 60000, # 100 per minute }, # Optional: Custom metadata meta={ "plan": "pro", "created_by": "admin", }, ) # Send result.key to your user (only time you'll see it!) print(f"New key: {result.key}") print(f"Key ID: {result.key_id}") ``` The full API key is only returned once at creation. Unkey stores only a hash. *** ## Update Keys Modify an existing key: ```python theme={null} unkey.keys.update_key( key_id="key_...", name="Updated name", meta={"plan": "enterprise"}, enabled=True, # Update rate limit ratelimit={ "limit": 1000, "duration": 60000, }, ) ``` *** ## Delete Keys Permanently revoke a key: ```python theme={null} unkey.keys.delete_key(key_id="key_...") ``` Or disable temporarily: ```python theme={null} unkey.keys.update_key( key_id="key_...", enabled=False, ) ``` *** ## Async Support All methods have async variants with `_async` suffix: ```python theme={null} import asyncio import os from unkey import Unkey async def main(): async with Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) as unkey: # Async verification result = await unkey.keys.verify_key_async(key="sk_live_...") if result.valid: print("Key is valid!") # Async key creation new_key = await unkey.keys.create_key_async( api_id="api_...", prefix="sk_live", ) print(f"Created: {new_key.key}") asyncio.run(main()) ``` ### Async FastAPI ```python theme={null} from fastapi import FastAPI, Header, HTTPException from unkey import Unkey import os app = FastAPI() @app.get("/api/protected") async def protected_route(x_api_key: str = Header(...)): async with Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) as unkey: result = await unkey.keys.verify_key_async(key=x_api_key) if not result.valid: raise HTTPException(status_code=401, detail=result.code) return {"message": "Access granted", "key_id": result.key_id} ``` *** ## Error Handling ```python theme={null} import os from unkey import Unkey, ApiError unkey = Unkey(root_key=os.environ["UNKEY_ROOT_KEY"]) try: result = unkey.keys.create_key(api_id="api_...") print(f"Created: {result.key}") except ApiError as e: print(f"API Error: {e.status_code} - {e.message}") # e.status_code - HTTP status # e.message - Human readable error # e.raw_response - Full response for debugging ``` *** ## Rate Limiting Rate limiting is included in key verification, but you can also use the standalone rate limit API: ```python theme={null} result = unkey.ratelimits.limit( namespace="my-app", identifier="user_123", limit=100, duration=60000, # 60 seconds ) if not result.success: print(f"Rate limited. Reset at: {result.reset}") else: print(f"Allowed. {result.remaining} requests left") ``` *** ## Full Reference Complete auto-generated API reference # @unkey/api Source: https://unkey.com/docs/libraries/ts/api Unkey's API provides programmatic access for all resources within our platform. ## SDK Installation The SDK can be installed with either [npm](https://www.npmjs.com/), [pnpm](https://pnpm.io/), [bun](https://bun.sh/) or [yarn](https://classic.yarnpkg.com/en/) package managers. ```bash npm theme={null} npm add @unkey/api ``` ```bash pnpm theme={null} pnpm add @unkey/api ``` ```bash bun theme={null} bun add @unkey/api ``` ```bash yarn theme={null} yarn add @unkey/api zod # Note that Yarn does not install peer dependencies automatically. You will need # to install zod as shown above. ``` This package is published with CommonJS and ES Modules (ESM) support. ## Requirements For supported JavaScript runtimes, please consult [RUNTIMES.md](https://github.com/unkeyed/sdks/blob/main/api/ts/RUNTIMES.md). ## SDK Example Usage ### Example ```typescript theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env["UNKEY_ROOT_KEY"], }); async function run() { const { meta, data } = await unkey.apis.createApi({ name: "payment-service-production", }); console.log(data); } run(); ``` ## Repository The full autogenerated documentation can be found on GitHub. # @unkey/cache Source: https://unkey.com/docs/libraries/ts/cache/overview Cache middleware with types ## Motivation Everyone needs caching, but it's often poorly implemented. Not from a technical perspective but from a usability perspective. Caching should be easy to use, typesafe, and composable. How caching looks like in many applications: ```ts theme={null} const cache = new Some3rdPartyCache(...) type User = { email: string }; let user = await cache.get("userId") as User | undefined | null; if (!user){ user = await database.get(...) await cache.set("userId", user, Date.now() + 60_000) } // use user ``` There are a few annoying things about this code: * Manual type casting * No support for stale-while-revalidate * Only checks a single cache Most people would build a small wrapper around this to make it easier to use and so did we: This library is the result of a rewrite of our own caching layer after some developers were starting to replicate it. It's used in production by Unkey any others. ## Features * **Typescript**: Fully typesafe * **Tiered Cache**: Multiple caches in series to fall back on * **Metrics**: Middleware for collecting metrics * **Stale-While-Revalidate**: Async loading of data from your origin * **Encryption**: Middleware for automatic encryption of cache values * **Composable**: Mix and match primitives to build what you need ## Quickstart ```bash npm theme={null} npm install @unkey/cache ``` ```bash pnpm theme={null} pnpm add @unkey/cache ``` ```bash yarn theme={null} yarn add @unkey/cache ``` ```bash bun theme={null} bun install @unkey/cache ``` ```ts Hello World theme={null} import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { MemoryStore } from "@unkey/cache/stores"; /** * Define the type of your data, * or perhaps generate the types from your database */ type User = { id: string; email: string; }; /** * In serverless you'd get this from the request handler * See /docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); const memory = new MemoryStore({ persistentMap: new Map() }); const cache = createCache({ user: new Namespace(ctx, { stores: [memory], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }) }); await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.get("userId") console.log(user) ``` ```ts Tiered Caches theme={null} import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { CloudflareStore, MemoryStore } from "@unkey/cache/stores"; /** * In serverless you'd get this from the request handler * See /docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); /** * Define the type of your data, or perhaps generate the types from your database */ type User = { id: string; email: string; }; const memory = new MemoryStore({ persistentMap: new Map() }); const cloudflare = new CloudflareStore({ domain: "cache.unkey.dev", zoneId: env.CLOUDFLARE_ZONE_ID!, cloudflareApiKey: env.CLOUDFLARE_API_KEY!, }); const cache = createCache({ user: new Namespace(ctx, { /** * Specifying first `memory`, then `cloudflare` will automatically check both stores * in order. * If a value is found in memory, it is returned, else it will check cloudflare, * and if it's found in cloudflare, the value is backfilled to memory. */ stores: [memory, cloudflare], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }); }); async function main() { await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.get("userId"); console.info(user); } main(); ``` ```ts Multiple Namespaces theme={null} import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { CloudflareStore, MemoryStore } from "@unkey/cache/stores"; /** * In serverless you'd get this from the request handler * See /docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); /** * Define the type of your data, or perhaps generate the types from your database */ type User = { id: string; email: string; }; const memory = new MemoryStore({ persistentMap: new Map() }); const cloudflare = new CloudflareStore({ domain: "cache.unkey.dev", zoneId: env.CLOUDFLARE_ZONE_ID!, cloudflareApiKey: env.CLOUDFLARE_API_KEY!, }); type ApiKey = { hash: string; ownerId: string; permissions: string[]; }; const cache = createCache({ user: new Namespace(ctx, { stores: [memory, cloudflare], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }), apiKey:new Namespace(ctx, { stores: [memory], fresh: 10_000, // Data is fresh for 10 seconds stale: 60_000, // Data is stale for 60 seconds }), }); async function main() { await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.get("userId"); console.info(user); await cache.apiKey.set("hash", {hash:"hash", ownerId: "me", permissions: ["do_many_things"]}) } main(); ``` ```ts theme={null} import { Namespace, createCache } from "@unkey/cache"; import { MemoryStore, CloudflareStore} from "@unkey/cache/stores"; /** * Define your data types. * You can hopefully reuse some of these from your database models. */ type User = { email: string; }; type Account = { name: string; }; /** * Configure the swr cache defaults. */ const fresh = 60_000; // fresh for 1 minute const stale = 900_000; // stale for 15 minutes /** * Create your store instances */ const memory = new MemoryStore({ persistentMap: new Map() }); const cloudflare = new CloudflareStore({ cloudflareApiKey: "", zoneId: "", domain: "", }) /** * Create your cache instance */ const cache = createCache({ account: new Namespace(ctx, { stores: [memory], fresh, // use the defaults defined above or a custom value stale, }), user: new Namespace(ctx, { // tiered cache, checking memory first, then cloudflare stores: [memory, cloudflare], fresh, stale, }), }); await cache.account.set("key", { name: "x" }); const user = await cache.user.get("user_123"); // typescript error, because `email` is not a key of `Account` await cache.account.set("key", { email: "x" }); ``` ## Concepts ### Namespaces Namespaces are a way to define the type of data in your cache and apply settings to it. They are used to ensure that you don't accidentally store the wrong type of data in a cache, which otherwise can happen easily when you're changing your data structures. Each namespace requires a type parameter and is instantiated with a set of stores and cache settings. ```ts Constructor theme={null} new Namespace(ctx, opts) ``` The type of data stored in this namespace, for example: ```ts theme={null} type User = { email: string; }; ``` An execution context, such as a request or a worker instance. [Read more](/libraries/ts/cache/overview#context) ```ts theme={null} interface Context { waitUntil: (p: Promise) => void; } ``` On Cloudflare workers or Vercel edge functions, you receive a context from the `fetch` handler. Otherwise you can use this: ```ts theme={null} import { DefaultStatefulContext } from "@unkey/cache"; const ctx = new DefaultStatefulContext(); ``` An array of stores to use for this namespace. When providing multiple stores, the cache will be checked in order of the array until a value is found or all stores have been checked. You should order the stores from fastest to slowest, so that the fastest store is checked first. The time in milliseconds that a value is considered fresh. Cache hits within this time will return the cached value. Must be less than `stale`. The time in milliseconds that a value is considered stale. Cache hits within this time will return the cached value and trigger a background refresh. Must be greater than `fresh`. ```ts Example namespace with two stores theme={null} import { Namespace, DefaultStatefulContext, MemoryStore, CloudflareStore } from "@unkey/cache"; type User = { email: string; } const memory = new MemoryStore({ persistentMap: new Map(), }); const cloudflare = new CloudflareStore({ cloudflareApiKey: c.env.CLOUDFLARE_API_KEY, zoneId: c.env.CLOUDFLARE_ZONE_ID, domain: "cache.unkey.dev", }) const ctx = new DefaultStatefulContext() const namespace = new Namespace(ctx, { stores: [memory, cloudflare], fresh: 60_000, stale: 900_000, }); ``` ### Tiered Cache Different caches have different characteristics, some may be fast but volatile, others may be slow but persistent. By using a tiered cache, you can combine the best of both worlds. In almost every case, you want to use a fast in-memory cache as the first tier. There is no reason not to use it, as it doesn't add any latency to your application. The goal of this implementation is that it's invisible to the user. Everything behaves like a single cache. You can add as many tiers as you want. #### Reading from the cache When using a tiered cache, all stores will be checked in order until a value is found or all stores have been checked. If a value is found in a store, it will be backfilled to the previous stores in the list asynchronously. ```mermaid theme={null} sequenceDiagram autonumber App->>Cache: get key Cache->>+Tier1: get key Tier1->>-Cache: undefined Cache->>+Tier2: get key Tier2->>-Cache: value Cache->>App: value Cache-->>Tier1: async set key value ``` #### Writing to the cache When setting or deleting a key, every store will be updated in parallel. ```mermaid theme={null} sequenceDiagram autonumber App->>Cache: set key value par Cache->>Tier1: set key value Tier1->>Cache: ack and Cache->>Tier2: set key value Tier2->>Cache: ack end Cache->>App: ack ``` #### Example ```ts theme={null} import { DefaultStatefulContext, Namespace, createCache } from "@unkey/cache"; import { CloudflareStore, MemoryStore } from "@unkey/cache/stores"; /** * In serverless you'd get this from the request handler * See https://unkey.com/docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); /** * Define the type of your data, or perhaps generate the types from your database */ type User = { id: string; email: string; }; const memory = new MemoryStore({ persistentMap: new Map() }); /** * @see https://unkey.com/docs/libraries/ts/cache/overview#cloudflare */ const cloudflare = new CloudflareStore({ domain: "cache.unkey.dev", zoneId: env.CLOUDFLARE_ZONE_ID!, cloudflareApiKey: env.CLOUDFLARE_API_KEY!, }); const userNamespace = new Namespace(ctx, { /** * Specifying first `memory`, then `cloudflare` will automatically check both stores in order * If a value is found in memory, it is returned, else it will check cloudflare, and if it's found * in cloudflare, the value is backfilled to memory. */ stores: [memory, cloudflare], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }); const cache = createCache({ user: userNamespace }); async function main() { await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.get("userId"); console.log(user); } main(); ``` ### Stale-While-Revalidate To make data fetching as easy as possible, the cache offers a `swr` method, that acts as a pull through cache. If the data is fresh, it will be returned from the cache, if it's stale it will be returned from the cache and a background refresh will be triggered and if it's not in the cache, the data will be synchronously fetched from the origin. ```ts theme={null} const user = await cache.user.swr("userId", async (userId) => { return database.exec("SELECT * FROM users WHERE id = ?", userId) }); ``` The cache key to fetch, just like when using `.get(key)` A callback function that will be called to fetch the data from the origin if it's stale or not in the cache. To understand what's happening under the hood, let's look at the different scenarios. `swr` works with tiered caches, but for simplicity, these charts may only show a single store. ```mermaid theme={null} sequenceDiagram autonumber App->>Cache: swr(key, loadFromOrigin) Cache->>+Tier1: get key Tier1->>-Cache: fresh value Cache->>App: value ``` ```mermaid theme={null} sequenceDiagram autonumber App->>Cache: swr(key, loadFromOrigin) Cache->>+Tier1: get key Tier1->>-Cache: stale value Cache->>App: value alt async Cache->>Origin: loadFromOrigin Origin->>Cache: value Cache->>Tier1: set key value end ``` ```mermaid theme={null} sequenceDiagram autonumber App->>Cache: swr(key, loadFromOrigin) Cache->>+Tier1: get key Tier1->>-Cache: undefined Cache->>+Tier2: get key Tier2->>-Cache: undefined Cache->>Origin: loadFromOrigin Origin->>Cache: value Cache->>App: value alt async Cache->>Tier1: set key value Cache->>Tier2: set key value end ``` #### Example ```ts theme={null} import { DefaultStatefulContext, Namespace, createCache } from "@unkey/cache" import { CloudflareStore, MemoryStore } from "@unkey/cache/stores"; /** * In serverless you'd get this from the request handler * See https://unkey.com/docs/libraries/ts/cache/overview#context */ const ctx = new DefaultStatefulContext(); /** * Define the type of your data, or perhaps generate the types from your database */ type User = { id: string; email: string; }; const memory = new MemoryStore({ persistentMap: new Map() }); /** * @see https://unkey.com/docs/libraries/ts/cache/overview#cloudflare */ const cloudflare = new CloudflareStore({ domain: "cache.unkey.dev", zoneId: env.CLOUDFLARE_ZONE_ID!, cloudflareApiKey: env.CLOUDFLARE_API_KEY!, }); const userNamespace = new Namespace(ctx, { /** * Specifying first `memory`, then `cloudflare` will automatically check both stores in order * If a value is found in memory, it is returned, else it will check cloudflare, and if it's found * in cloudflare, the value is backfilled to memory. */ stores: [memory, cloudflare], fresh: 60_000, // Data is fresh for 60 seconds stale: 300_000, // Data is stale for 300 seconds }); const cache = createCache({ user: userNamespace }); async function main() { await cache.user.set("userId", { id: "userId", email: "user@email.com" }); const user = await cache.user.swr("userId", async (userId)=>{ // @ts-expect-error we don't have a db in this example return db.getUser(userId) }); console.info(user); } main(); ``` ### Context In serverless functions it's not always trivial to run some code after you have returned a response. This is where the context comes in. It allows you to register promises that should be awaited before the function is considered done. Fortunately many providers offer a way to do this. In order to be used in this cache library, the context must implement the following interface: ```ts theme={null} export interface Context { waitUntil: (p: Promise) => void; } ``` For stateful applications, you can use the `DefaultStatefulContext`: ```ts theme={null} import { DefaultStatefulContext } from "@unkey/cache"; const ctx = new DefaultStatefulContext() ``` Vendor specific documentation: * [Cloudflare Workers](https://developers.cloudflare.com/workers/runtime-apis/context/) * [Vercel Serverless](https://vercel.com/docs/functions/functions-api-reference#waituntil) * [Vercel Edge and Middleware](https://vercel.com/docs/functions/edge-middleware/middleware-api#waituntil) ## Primitives ### Stores Stores are the underlying storage mechanisms for your cache. They can be in-memory, on-disk, or remote. You can use multiple stores in a namespace to create a tiered cache. The order of stores in a namespace is important. The cache will check the stores in order until it finds a value or all stores have been checked. You can create your own store by implementing the `Store` interface. [Read more.](/libraries/ts/cache/interface/store) Below are the available stores: #### Memory The memory store is an in-memory cache that is fast but only as persistent as your memory. In serverless environments, this means that the cache is lost when the function is cold-started. ```ts theme={null} import { MemoryStore } from "@unkey/cache/stores"; const memory = new MemoryStore({ persistentMap: new Map(), }); ``` Ensure that the `Map` is instantiated in a persistent scope of your application. For Cloudflare workers or serverless functions in general, this is the global scope. #### Cloudflare The Cloudflare store uses cloudflare's [`Cache` API](https://developers.cloudflare.com/workers/runtime-apis/cache/) to store cache values. This is a remote cache that is shared across all instances of your worker but isolated per datacenter. It's still pretty fast, but needs a network request to access the cache. ```ts theme={null} import { CloudflareStore } from "@unkey/cache/stores"; const cloudflare = new CloudflareStore({ cloudflareApiKey: "", zoneId: "", domain: "", cacheBuster: "", }) ``` The Cloudflare API key to use for cache purge operations. The api key must have the `Cache Purge` permission. You can create a new API token with this permission in the [Cloudflare dashboard](https://dash.cloudflare.com/profile/api-tokens). The Cloudflare zone ID where the cache is stored. You can find this in the Cloudflare dashboard. The domain to use for the cache. This must be a valid domain within the zone specified by `zoneId`. If the domain is not valid in the specified zone, the cache will not work and cloudflare does not provide an error message. You will just get cache misses. For example, we use `domain: "cache.unkey.dev"` in our API. As your data changes, it is important to keep backwards compatibility in mind. If your cached values are no longer backwards compatible, it can cause problems. For example when a value changes from optional to required. In these cases you should purge the entire cache by setting a new `cacheBuster` value. The `cacheBuster` is used as part of the cache key and changes ensure you are not reading old data anymore. #### Upstash Redis The Upstash Redis store uses the [Serverless Redis](https://upstash.com/docs/redis/overall/getstarted) offering from Upstash to store cache values. This is a serverless database with Redis compatibility. ```ts theme={null} import { UpstashRedisStore } from "@unkey/cache/stores"; import { Redis } from "@upstash/redis"; const redis = new Redis({ url: , token: , }) const redisStore = new UpstashRedisStore({ redis }) ``` The Upstash Redis client to use for cache operations. #### libSQL (Turso) The libSQL store can use an [embedded SQLite database](https://docs.turso.tech/features/embedded-replicas/introduction), or a remote [Turso](https://turso.tech) database to store cache values. You must create a table in your Turso database with the following schema: ```sql theme={null} CREATE TABLE IF NOT EXISTS cache ( key TEXT PRIMARY KEY, value TEXT NOT NULL, freshUntil INTEGER NOT NULL, staleUntil INTEGER NOT NULL ); ``` ```ts Remote Only theme={null} import { LibSQLStore } from "@unkey/cache/stores"; import { createClient } from "@libsql/client"; const client = createClient({ url: "libsql://...", authToken: "...", }); const store = new LibSQLStore({ client, }); ``` ```ts Embedded Replicas theme={null} import { LibSQLStore } from "@unkey/cache/stores"; import { createClient } from "@libsql/client"; const client = createClient({ url: "file:dev.db", syncUrl: "libsql://...", authToken: "...", }); const store = new LibSQLStore({ client, }); ``` The [libSQL client](https://docs.turso.tech/sdk/ts) to use for cache operations. The name of the database table name to use for cache operations. ### Middlewares #### Metrics The metrics middleware collects metrics about cache hits, misses, and backfills. It's useful for debugging and monitoring your cache usage. Using the metrics middleware requires a metrics sink. You can build your own sink by implementing the `Metrics` interface. For example we are using [axiom](https://axiom.co?ref=unkey). ```ts theme={null} interface Metrics = Record> { /** * Emit a new metric event * */ emit(metric: TMetric): void; /** * flush persists all metrics to durable storage. * You must call this method before your application exits, metrics are not persisted automatically. */ flush(): Promise; } ``` Wrap your store with the metrics middleware to start collecting metrics. ```ts theme={null} import { withMetrics } from "@unkey/cache/middleware"; const metricsSink = // your metrics sink const metricsMiddleware = withMetrics(metricsSink); const memory = new MemoryStore({ persistentMap: new Map() }); new Namespace(ctx, { // Wrap the store with the metrics middleware stores: [metricsMiddleware.wrap(memory)], // ... }); ``` The following metrics are emitted: ```ts theme={null} type Metric = | { metric: "metric.cache.read"; key: string; hit: boolean; status?: "fresh" | "stale"; latency: number; tier: string; namespace: string; } | { metric: "metric.cache.write"; key: string; latency: number; tier: string; namespace: string; } | { metric: "metric.cache.remove"; key: string; latency: number; tier: string; namespace: string; }; ``` #### Encryption When dealing with sensitive data, you might want to encrypt your cache values at rest. You can encrypt a store by wrapping it with the `EncryptedStore`. All you need is a 32 byte base64 encoded key. You can generate one with openssl: ```bash Generate a new encryption key theme={null} openssl rand -base64 32 ``` ```ts Example theme={null} import { withEncryption } from "@unkey/cache/middleware"; const encryptionKey = "" const encryptionMiddleware = await withEncryption(encryptionKey) const memory = new Memory({..}) // or any other store const store = encryptionMiddleware.wrap(memory); ``` Values will be encrypted using `AES-256-GCM` and persisted in the underlying store. You can rotate your encryption key at any point, but this will essentially purge the cache. A SHA256 hash of the encryption key is used in the cache key, to allow for rotation without causing decryption errors. ## Contributing If you have a store or middleware you'd like to see in this library, please open an [issue](https://github.com/unkeyed/unkey/issues/new) or a pull request. # @unkey/hono Source: https://unkey.com/docs/libraries/ts/hono Hono.js middleware for authenticating API keys > Hono - \[炎] means flame🔥 in Japanese - is a small, simple, and ultrafast web framework for the Edges. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute\@Edge, Deno, Bun, Vercel, Netlify, Lagon, AWS Lambda, Lambda\@Edge, and Node.js. `@unkey/hono` offers a middleware for authenticating API keys with [unkey](https://unkey.com). ## Install ```bash npm theme={null} npm install @unkey/hono ``` ```bash pnpm theme={null} pnpm add @unkey/hono ``` ```bash yarn theme={null} yarn add @unkey/hono ``` ```bash bun theme={null} bun install @unkey/hono ``` Let's dive straight in. The minimal setup looks like this. You need a root key with permission to verify keys. Go to [/settings/root-keys](https://app.unkey.com/settings/root-keys) and create a key with the `verify_key` permission. By default it tries to grab the API key from the `Authorization` header and then verifies it with Unkey. The result of the verification will be written to the context and can be accessed with `c.get("unkey")`. ```ts theme={null} import { Hono } from "hono" import { type UnkeyContext, unkey } from "@unkey/hono"; const app = new Hono<{ Variables: { unkey: UnkeyContext } }>(); app.use("*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY! })); app.get("/somewhere", (c) => { // access the unkey response here to get metadata of the key etc const keyInfo = c.get("unkey"); return c.text(`Hello ${keyInfo.identity?.externalId ?? "user"}!`); }) ``` ## Customizing the middleware ### Header By default the middleware tries to grab the api key from the `Authorization` header. You can change this by passing a custom header name to the middleware. ```ts theme={null} app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, getKey: (c) => c.req.header("x-api-key"), }) ); ``` If the header is missing the middleware will return a `401` error response like this ```ts theme={null} c.json({ error: "unauthorized" }, { status: 401 }); ``` To customize the response in case the header is missing, just return a response from the `getKey` function. ```ts theme={null} app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, getKey: (c) => { const key = c.req.header("x-api-key"); if (!key) { return c.text("missing api key", 401); } return key; }, }) ); ``` ### Handle errors ```ts theme={null} app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, onError: (c, err) => { // handle error return c.text("unauthorized", 401); }, }) ); ``` ### Handle invalid keys By default the middleware will not do anything with the verification response other than writing it to the context. However you most likely would like to just return a `401` response if the key is invalid and not continue with the request. To do this you can pass a `handleInvalidKey` handler to the middleware. See [here](/api-reference/v2/keys/create-api-key) for the full `response` object. ```ts theme={null} app.use( "*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, handleInvalidKey: (c, result) => { return c.json( { error: "unauthorized", reason: result.code, }, 401 ); }, }) ); ``` ### Pass verification tags You can pass tags to the verification request to help you filter keys later. ```ts theme={null} (c, next) => unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, tags: [`path=${c.req.path}`], })(c, next); ``` # @unkey/nextjs Source: https://unkey.com/docs/libraries/ts/nextjs Next.js SDK for Unkey The official Next.js SDK for Unkey. Use this within your [route handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) as a simple, type-safe way to verify API keys. ## Install ```bash npm theme={null} npm install @unkey/nextjs ``` ```bash pnpm theme={null} pnpm add @unkey/nextjs ``` ```bash yarn theme={null} yarn add @unkey/nextjs ``` ```bash bun theme={null} bun add @unkey/nextjs ``` Protecting API routes is as simple as wrapping them with the `withUnkey` handler: ```ts theme={null} import { NextRequestWithUnkeyContext, withUnkey } from "@unkey/nextjs"; export const POST = withUnkey( async (req: NextRequestWithUnkeyContext) => { // The key has already been verified at this point // Access verification details via req.unkey.data if (!req.unkey.data.valid) { return new Response("Unauthorized", { status: 401 }); } // Your API logic here return Response.json({ message: "Hello!", keyId: req.unkey.data.keyId, // If you set an externalId when creating the key: externalId: req.unkey.data.identity?.externalId, }); }, { rootKey: process.env.UNKEY_ROOT_KEY! } ); ``` ### What's in `req.unkey`? The `req.unkey.data` object contains the verification result: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------ | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Roles attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | Access these via `req.unkey.data.valid`, `req.unkey.data.keyId`, etc. If you want to customize how `withUnkey` processes incoming requests, you can do so as follows: ### `getKey` By default, withUnkey will look for a bearer token located in the `authorization` header. If you want to customize this, you can do so by passing a getter in the configuration object: ```ts theme={null} export const GET = withUnkey( async (req) => { // ... }, { rootKey: process.env.UNKEY_ROOT_KEY!, getKey: (req) => new URL(req.url).searchParams.get("key"), } ); ``` ### `onError` You can specify custom error handling. By default errors will be logged to the console, and `withUnkey` will return a NextResponse with status 500. ```ts theme={null} export const GET = withUnkey( async (req) => { // ... }, { rootKey: process.env.UNKEY_ROOT_KEY!, onError: async (req, res) => { await analytics.trackEvent(`Error ${res.code}: ${res.message}`); return new NextResponse("Unkey error", { status: 500 }); }, } ); ``` ### `handleInvalidKey` Specify what to do if Unkey reports that your key is invalid. ```ts theme={null} export const GET = withUnkey( async (req) => { // ... }, { rootKey: process.env.UNKEY_ROOT_KEY!, handleInvalidKey: (req, res) => { return new Response("Unauthorized", { status: 401 }); }, } ); ``` # Overview Source: https://unkey.com/docs/libraries/ts/overview Complete guide to using Unkey in TypeScript and JavaScript applications Unkey provides two TypeScript packages: * **@unkey/api** — Manage keys, APIs, and rate limits (server-side, requires root key) * **@unkey/ratelimit** — Standalone rate limiting (server-side, requires root key) For framework-specific wrappers, see [@unkey/nextjs](/libraries/ts/nextjs) and [@unkey/hono](/libraries/ts/hono). ## Installation ```bash npm theme={null} npm install @unkey/api @unkey/ratelimit ``` ```bash pnpm theme={null} pnpm add @unkey/api @unkey/ratelimit ``` ```bash bun theme={null} bun add @unkey/api @unkey/ratelimit ``` ## Quick Start ### Initialize the client ```typescript theme={null} import { Unkey } from "@unkey/api"; import { Ratelimit } from "@unkey/ratelimit"; // For key management const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, }); // For rate limiting const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "my-app", limit: 10, duration: "60s", }); ``` Never expose your root key in client-side code. These SDKs are for server-side use only. *** ## Verify an API Key The most common operation — check if a user's API key is valid: ```typescript theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); async function handleRequest(request: Request) { const apiKey = request.headers.get("x-api-key"); if (!apiKey) { return new Response("Missing API key", { status: 401 }); } try { const { meta, data } = await unkey.keys.verifyKey({ key: apiKey, }); if (!data.valid) { // Key is invalid, expired, rate limited, etc. return new Response(`Unauthorized: ${data.code}`, { status: 401 }); } // Key is valid — access granted // data.identity?.externalId, data.meta, data.credits, etc. available return handleAuthenticatedRequest(request, data); } catch (err) { console.error(err); return new Response("Service unavailable", { status: 503 }); } } ``` ### Verification response | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------ | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Roles attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ### Check permissions during verification ```typescript theme={null} try { const { meta, data } = await unkey.keys.verifyKey({ key: apiKey, permissions: "documents.write", // Required permission }); if (!data.valid && data.code === "INSUFFICIENT_PERMISSIONS") { return new Response("Forbidden", { status: 403 }); } } catch (err) { console.error(err); return new Response("Service unavailable", { status: 503 }); } ``` *** ## Create API Keys Issue new keys for your users: ```typescript theme={null} try { const { meta, data } = await unkey.keys.create({ apiId: "api_...", // Optional but recommended prefix: "sk_live", // Visible prefix externalId: "user_123", // Link to your user name: "Production key", // Human-readable name // Optional limits expires: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days remaining: 1000, // Usage limit refill: { amount: 1000, interval: "monthly", // Auto-refill }, ratelimits: [{ name: "requests", limit: 100, duration: 60000, // 100/minute }], // Optional metadata meta: { plan: "pro", createdBy: "admin", }, }); // Send data.key to your user (only time you'll see the full key!) console.log("New key:", data.key); console.log("Key ID:", data.keyId); } catch (err) { console.error(err); throw new Error("Failed to create key"); } ``` The full API key is only returned once at creation. Unkey stores only a hash. Make sure to display it to your user immediately. *** ## Update Keys Modify an existing key's configuration: ```typescript theme={null} try { await unkey.keys.update({ keyId: "key_...", // Any fields you want to change name: "Updated name", meta: { plan: "enterprise" }, enabled: true, expires: Date.now() + 90 * 24 * 60 * 60 * 1000, ratelimits: [{ name: "requests", limit: 1000, // Upgraded limit duration: 60000, }], }); } catch (err) { console.error(err); throw new Error("Failed to update key"); } ``` *** ## Delete Keys Permanently revoke a key: ```typescript theme={null} try { await unkey.keys.delete({ keyId: "key_...", }); } catch (err) { console.error(err); throw new Error("Failed to delete key"); } ``` Or disable temporarily (can re-enable later): ```typescript theme={null} try { await unkey.keys.update({ keyId: "key_...", enabled: false, }); } catch (err) { console.error(err); throw new Error("Failed to disable key"); } ``` *** ## Rate Limiting Use `@unkey/ratelimit` for standalone rate limiting: ```typescript theme={null} import { Ratelimit } from "@unkey/ratelimit"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "60s", }); async function handleRequest(request: Request) { const userId = request.headers.get("x-user-id") ?? "anonymous"; try { const { success, remaining, reset } = await limiter.limit(userId); if (!success) { return new Response("Rate limit exceeded", { status: 429, headers: { "Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(), "X-RateLimit-Remaining": "0", }, }); } // Process request... } catch (err) { console.error(err); return new Response("Rate limit service unavailable", { status: 503 }); } } ``` ### Cost-based rate limiting Expensive operations can consume more of the limit: ```typescript theme={null} // Normal request costs 1 await limiter.limit(userId); // Expensive request costs 10 await limiter.limit(userId, { cost: 10 }); ``` *** ## Error Handling Use try/catch to handle errors: ```typescript theme={null} try { const { meta, data } = await unkey.keys.create({ ... }); console.log("Request ID:", meta.requestId); console.log("Key ID:", data.keyId); } catch (err) { console.error(err); throw new Error("Failed to create key"); } ``` ### Resilient verification For production, handle edge cases: ```typescript theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }); async function verifyApiKey(key: string): Promise { try { const { meta, data } = await unkey.keys.verifyKey({ key }); return data; } catch (err) { // Network error, timeout, etc. console.error("Unkey verification error:", err); return null; // Or return a default allow/deny } } ``` *** ## TypeScript Types The SDK is fully typed. Import types as needed: ```typescript theme={null} import type { VerifyKeyResult, Key, Api, RatelimitResponse, } from "@unkey/api"; ``` *** ## Framework Guides withUnkey wrapper for API routes Middleware for Hono apps Step-by-step Express guide Step-by-step Bun guide *** ## Full Reference Complete auto-generated API reference # Delete Override Source: https://unkey.com/docs/libraries/ts/ratelimit/override/delete-override Deletes an override ## Request Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers, More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) Either `namespaceId` or `namespaceName` is required. Not both. The id of the namespace. Either namespaceId or namespaceName must be provided Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) ## Response No response, but if no error is returned the override has been deleted successfully. ```ts theme={null} try { const { meta, data } = await unkey.deleteOverride({ identifier: "user_123", namespaceName: "email.outbound", }); } catch (err) { console.error(err); throw err; } ``` ```ts theme={null} try { const { meta, data } = await unkey.deleteOverride({ identifier: "user_123", namespaceId:"rlns_12345", }); } catch (err) { console.error(err); throw err; } ``` # Get Override Source: https://unkey.com/docs/libraries/ts/ratelimit/override/get-override Gets a ratelimit override ## Request Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers, More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) Either `namespaceId` or `namespaceName` is required. Not both. The id of the namespace. Either namespaceId or namespaceName must be provided Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) ## Response Identifier of the override requested Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers, More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) How many requests may pass in a given window. The window duration in milliseconds. ```ts theme={null} const { meta, data } = await unkey.getOverride({ identifier:"user.example", namespaceName: "email.outbound" }); ``` ```ts theme={null} const { meta, data } = await unkey.getOverride({ identifier:"user.example", namespaceId: "rlns_1234", }); ``` ```ts theme={null} { result: { id: "rlor_4567", identifier: "user.example", limit: 10, duration: 60000, async: false } } ``` # List Overrides Source: https://unkey.com/docs/libraries/ts/ratelimit/override/list-overrides Lists all overrides ## Request Either `namespaceId` or `namespaceName` is required. Not both. The id of the namespace. Either namespaceId or namespaceName must be provided Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) ## Response Identifier of the override requested Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers, More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) How many requests may pass in a given window. The window duration in milliseconds. The total number of overrides The cursor to use for pagination ```ts theme={null} const { meta, data } = await unkey.listOverrides({ namespaceName: "email.outbound" }); ``` ```ts theme={null} const { meta, data } = await unkey.listOverrides({ nameSpaceId:"rlns_12345", }); ``` ```ts theme={null} { result: { overrides: [ { id: 'rlor_1234', identifier: 'customer_123', limit: 10, duration: 50000, async: false } ], total: 1, cursor: 'rlor_1234' } } ``` # Rate Limit Overrides Source: https://unkey.com/docs/libraries/ts/ratelimit/override/overview Set custom limits for specific identifiers Ratelimit overrides are a way to override the ratelimit for specific users or group using an identifier. ## Configure your override ```ts theme={null} import { Override } from "@unkey/ratelimit" const unkey = new Override({ rootKey: process.env.UNKEY_ROOT_KEY, }) ``` ## Use it ```ts theme={null} async function handler(request) { const identifier = request.getUserId() // or ip or anything else you want try { const { meta, data } = await unkey.setOverride({ identifier: identifier, limit: 10, duration: 60000, namespaceName: "email.outbound", }) if (data.error){ // handle the error here console.error(data.error.message); return; } } catch (err) { console.error(err); return; } // handle the request here } ``` There are four main functions to interact with overrides: * [setOverride](/libraries/ts/ratelimit/override/set-override) Sets an override for a ratelimit. * [getOverride](/libraries/ts/ratelimit/override/get-override) Gets a ratelimit override. * [deleteOverride](/libraries/ts/ratelimit/override/delete-override) Deletes an override. * [listOverrides](/libraries/ts/ratelimit/override/list-overrides) Lists all overrides for a namnespace. # Set Override Source: https://unkey.com/docs/libraries/ts/ratelimit/override/set-override Sets an override for a ratelimit ## Request Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( \* ) can be used to match multiple identifiers, More info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) How many requests may pass in a given window. The window duration in milliseconds. Either `namespaceId` or `namespaceName` is required. Not both. The id of the namespace. Either namespaceId or namespaceName must be provided Namespaces group different limits together for better analytics. You might have a namespace for your public API and one for internal tRPC routes. Wildcards can also be used, more info can be found at [https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules](https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules) ## Response The id of the override that was set. ```ts theme={null} const { meta, data } = await unkey.setOverride({ identifier: "user_123", limit: 10, duration: 60000, namespaceName: "email.outbound", async: true }) ``` ```ts theme={null} const { meta, data } = await unkey.setOverride({ identifier: "user_123", limit: 5, duration: 50000, namespaceId: "rlns_1234", async: false }) ``` ```ts theme={null} { result: { overrideId: 'rlor_12345' } } ``` # Ratelimit Source: https://unkey.com/docs/libraries/ts/ratelimit/ratelimit Serverless ratelimiting `@unkey/ratelimit` is a library for fast global ratelimiting in serverless functions. ## Install ```bash npm theme={null} npm install @unkey/ratelimit ``` ```bash pnpm theme={null} pnpm add @unkey/ratelimit ``` ```bash yarn theme={null} yarn add @unkey/ratelimit ``` ```bash bun theme={null} bun install @unkey/ratelimit ``` ## Configure your ratelimiter ```ts theme={null} import { Ratelimit } from "@unkey/ratelimit" const unkey = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "my-app", limit: 10, duration: "30s", }) ``` ## Use it ```ts theme={null} async function handler(request) { const identifier = request.getUserId() // or ip or anything else you want const ratelimit = await unkey.limit(identifier) if (!ratelimit.success){ return new Response("try again later", { status: 429 }) } // handle the request here } ``` ## Making it bullet proof Everything we do is built for scale and stability. We built on some of the world's most stable platforms ([Planetscale](https://planetscale.com/) and [Cloudflare](https://www.cloudflare.com)) and run an extensive test suite before and after every deployment. Even so, we would be fools if we wouldn't explain how you can put in safe guards along the way. In case of severe network degredations or other unforseen events, you might want to put an upper bound on how long you are willing to wait for a response from unkey. By default the SDK will reject a request if it hasn't received a response from unkey within 5 seconds. You can tune this via the `timeout` config in the constructor (see below). The SDK captures most errors and handles them on its own, but we also encourage you to add a `onError` handler to configure what happens in case something goes wrong. Both `fallback` property of the `timeout` config and `onError` config are callback functions. They receive the original request identifier as one of their parameters, which you can use to determine whether to reject the request. ```ts theme={null} import { Ratelimit } from "@unkey/ratelimit" // In this example we decide to let requests pass, in case something goes wrong. // But you can of course also reject them if you want. const fallback = (identifier: string) => ({ success: true, limit: 0, reset: 0, remaining: 0 }) const unkey = new Ratelimit({ // ... standard stuff timeout: { ms: 3000, // only wait 3s at most before returning the fallback fallback }, onError: (err, identifier) => { console.error(`${identifier} - ${err.message}`) return fallback(identifier) } }) const { success } = await unkey.limit(identifier) ``` *** ## API ### `new Ratelimit(config: RatelimitConfig)` Create a new instance for ratelimiting by providing the necessary configuration. How many requests may pass in the given duration. How long the window should be. Either a type string literal like `60s`, `20m` or plain milliseconds. The unkey root key. You can create one at [app.unkey.com/settings/root-keys](https://app.unkey.com/settings/root-keys) Make sure the root key has permissions to use ratelimiting. Namespaces allow you to separate different areas of your app and have isolated limits. Make sure the root key has permissions to use ratelimiting. Configure a timeout to prevent network issues from blocking your function for too long. Disable it by setting `timeout: false` Timeouts rely on `Date.now()`. In cloudflare workers time doesn't progress unless there is some io happening, which means the timeout might not work as expected. Other runtimes are working. Time in milliseconds until the response is returned. A custom response to return when the timeout is reached. The important bit is the `success` value, choose whether you want to let requests pass or not. Configure what happens for unforeseen errors Example letting requests pass: ```ts theme={null} onError: ()=> ({ success: true, limit: 0, remaining: 0, reset: 0}) ``` Example rejecting the request: ```ts theme={null} onError: ()=> ({ success: true, limit: 0, remaining: 0, reset: 0}) ``` ### `.limit(identifier: string, opts: LimitOptions): Promise` Check whether a specific identifier is currently allowed to do something or if they have currently exceeded their limit. Expensive requests may use up more resources. You can specify a cost to the request and we'll deduct this many tokens in the current window. If there are not enough tokens left, the request is denied. **Example:** 1. You have a limit of 10 requests per second you already used 4 of them in the current window. 2. Now a new request comes in with a higher cost: ```ts theme={null} const res = await rl.limit("identifier", { cost: 4 }) ``` 3. The request passes and the current limit is now at `8` 4. The same request happens again, but would not be rejected, because it would exceed the limit in the current window: `8 + 4 > 10` ### `RatelimitResponse` Whether the request may pass(true) or exceeded the limit(false). Maximum number of requests allowed within a window. How many requests the user has left within the current window. Unix timestamp in milliseconds when the limits are reset. # Overview Source: https://unkey.com/docs/migrations/introduction Move your existing API keys to Unkey without disrupting users Already have API keys in production? Migrate them to Unkey without requiring your users to generate new keys. ## How migration works Extract key hashes from your current system (database, auth provider, etc.) Contact us to set up your migration and receive a `migrationId` Use the migration API to import your key hashes with their configuration Point your API verification to Unkey — existing keys work immediately ## Why key hashes? Unkey never stores plaintext keys. During migration, you provide the **hash** of each key, not the key itself. Your users continue using their existing keys. When they call your API: 1. You extract the key from the request 2. Unkey hashes it and matches against stored hashes 3. Verification succeeds if there's a match **Result:** Zero changes required from your users. ## What can be migrated? | Setting | Migrated? | | ------------------- | --------- | | Key hash | ✅ | | Custom metadata | ✅ | | Roles & permissions | ✅ | | Rate limits | ✅ | | Credits/remaining | ✅ | | Expiration | ✅ | | Identity/owner | ✅ | ## Migration paths Export hashes from PostgreSQL, MySQL, MongoDB, etc. Migrate from Auth0, Clerk, Firebase, or custom auth. Moving from Kong, Tyk, or homegrown solutions. We'll help you figure out the best approach. ## Get started Email [support@unkey.com](mailto:support@unkey.com) with: * Your workspace ID * Current key storage system * Approximate number of keys * Hash algorithm used (SHA-256, bcrypt, etc.) We'll help you design a migration strategy that minimizes risk. Follow our [step-by-step guide](/migrations/keys) to migrate your keys. Every system is different. We're here to help — reach out at [support@unkey.com](mailto:support@unkey.com) and we'll guide you through your specific migration. ## Next steps Step-by-step instructions for migrating keys Full v2 API documentation # Migrate Keys to Unkey Source: https://unkey.com/docs/migrations/keys Step-by-step guide to importing existing API keys This guide walks you through migrating existing API keys to Unkey. ## Prerequisites Sign up at [app.unkey.com](https://app.unkey.com/auth/sign-up) Note your `workspaceId` and `apiId`: * **Workspace ID**: Settings → General (upper right corner) Workspace settings page * **API ID**: APIs → Your API (upper right corner) API page Go to Settings → Root Keys and create one with `api.*.create_key` and `api.*.verify_key` permissions. Root key creation Email [support@unkey.com](mailto:support@unkey.com) with: * Your workspace ID * Source system (PostgreSQL, Auth0, etc.) * Hash algorithm used We'll send you a `migrationId`. ## Supported hash formats | Variant | Description | | --------------- | ----------------------- | | `sha256_base64` | SHA-256, base64 encoded | | `sha256_hex` | SHA-256, hex encoded | | `bcrypt` | bcrypt hash | Need a different hash format? Contact us — we can add support for your algorithm. ## Export your keys Extract key hashes from your current system. **Never include plaintext keys.** ### Example: PostgreSQL ```sql theme={null} SELECT key_hash, user_id, created_at, metadata FROM api_keys WHERE revoked = false; ``` ### Example: MongoDB ```javascript theme={null} db.apiKeys.find( { revoked: false }, { hash: 1, userId: 1, metadata: 1 } ); ``` ## Hash your keys (if not already hashed) If you have plaintext keys, hash them before migration: ```javascript theme={null} const { createHash } = require("node:crypto"); function hashKey(plaintext) { return { value: createHash("sha256").update(plaintext).digest("base64"), variant: "sha256_base64", }; } ``` ## Migrate to Unkey Use the migration API to import your keys: ```javascript theme={null} const UNKEY_ROOT_KEY = process.env.UNKEY_ROOT_KEY; const MIGRATION_ID = "mig_..."; // From support const API_ID = "api_..."; // Your exported keys const keysToMigrate = [ { hash: { value: "abc123...", // The hash value variant: "sha256_base64", }, externalId: "user_123", // Link to your user meta: { plan: "pro", migratedFrom: "legacy-system", }, ratelimits: [{ name: "requests", limit: 1000, duration: 60000, }], }, // ... more keys ]; const response = await fetch("https://api.unkey.com/v2/keys.migrateKeys", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${UNKEY_ROOT_KEY}`, }, body: JSON.stringify({ migrationId: MIGRATION_ID, apiId: API_ID, keys: keysToMigrate, }), }); const result = await response.json(); if (!response.ok) { console.error("Migration failed:", result?.error || `HTTP ${response.status}`); return; } if (!result?.data) { console.error("Migration response missing data"); return; } console.log("Migrated:", result.data.migrated.length); console.log("Failed:", result.data.failed.length); ``` ## Response ```json theme={null} { "meta": { "requestId": "req_..." }, "data": { "migrated": [ { "hash": "abc123...", "keyId": "key_..." } ], "failed": [ { "hash": "xyz789...", "error": "Key already exists" } ] } } ``` ## Key configuration options Each key in the migration can include: | Field | Description | | ------------- | ---------------------------------------------------- | | `hash` | Required. The hash object with `value` and `variant` | | `prefix` | Key prefix (for display only, not verified) | | `name` | Human-readable name | | `externalId` | Link to your user/identity | | `meta` | Custom JSON metadata | | `roles` | Array of role names | | `permissions` | Array of permission names | | `ratelimits` | Rate limit configuration | | `credits` | Usage limit configuration | | `expires` | Expiration timestamp (Unix ms) | | `enabled` | Whether key is active (default: true) | ## Batch migrations For large migrations, batch your requests: ```javascript theme={null} const BATCH_SIZE = 100; async function migrateAll(allKeys) { const results = { migrated: 0, failed: 0 }; for (let i = 0; i < allKeys.length; i += BATCH_SIZE) { const batch = allKeys.slice(i, i + BATCH_SIZE); const response = await fetch("https://api.unkey.com/v2/keys.migrateKeys", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${UNKEY_ROOT_KEY}`, }, body: JSON.stringify({ migrationId: MIGRATION_ID, apiId: API_ID, keys: batch, }), }); const result = await response.json(); const batchNum = Math.floor(i / BATCH_SIZE) + 1; if (!response.ok) { console.error(`Batch ${batchNum} failed:`, result?.error || `HTTP ${response.status}`); results.failed += batch.length; continue; } if (!result?.data) { console.error(`Batch ${batchNum} missing data`); results.failed += batch.length; continue; } results.migrated += result.data.migrated.length; results.failed += result.data.failed.length; console.log(`Batch ${batchNum}: ${result.data.migrated.length} migrated`); } return results; } ``` ## Update your verification After migration, update your API to verify keys with Unkey: ```javascript theme={null} // Before: Custom verification const isValid = await myDatabase.verifyKey(apiKey); // After: Unkey verification const response = await fetch("https://api.unkey.com/v2/keys.verifyKey", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + process.env.UNKEY_ROOT_KEY }, body: JSON.stringify({ key: apiKey }), }); const result = await response.json(); if (!response.ok || !result?.data) { // Handle error appropriately return { valid: false, error: result?.error || "Verification failed" }; } const isValid = result.data.valid; ``` ## Rollback plan Keep your old verification system running in parallel during migration: ```javascript theme={null} async function verifyKey(apiKey) { // Try Unkey first const unkeyResult = await verifyWithUnkey(apiKey); if (unkeyResult.valid) { return unkeyResult; } // Fall back to legacy system (temporary) const legacyResult = await verifyWithLegacy(apiKey); if (legacyResult.valid) { const maskedKey = apiKey.slice(0, 5) + "..." + apiKey.slice(-5); console.log("Key found in legacy, not yet migrated:", maskedKey); } return legacyResult; } ``` Remove the fallback once migration is complete and verified. ## Need help? We'll help you plan and execute your migration safely. # Bun Source: https://unkey.com/docs/quickstart/apis/bun Protect your Bun server with Unkey API key authentication ## What you'll build A Bun HTTP server that requires a valid API key on every request. Invalid or missing keys get rejected with a 401. **Time to complete:** \~3 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [API created](https://app.unkey.com/apis) in your Unkey dashboard * [Bun](https://bun.sh) installed Clone the complete example and run it locally. ```bash theme={null} mkdir unkey-bun && cd unkey-bun bun init -y ``` ```bash theme={null} bun add @unkey/api ``` Create a `.env` file with your credentials from the [Unkey dashboard](https://app.unkey.com/settings/root-keys): ```bash .env theme={null} UNKEY_ROOT_KEY="unkey_..." ``` Replace the contents of `index.ts`: ```ts index.ts theme={null} import { verifyKey } from "@unkey/api"; const server = Bun.serve({ async fetch(req) { // 1. Extract the key from the Authorization header const key = req.headers.get("Authorization")?.replace("Bearer ", ""); if (!key) { return Response.json({ error: "Missing API key" }, { status: 401 }); } // 2. Verify with Unkey try { const { meta, data } = await verifyKey({ key }); if (!data.valid) { // Key is invalid, expired, rate limited, etc. return Response.json( { error: "Invalid API key", code: data.code }, { status: 401 } ); } // 3. Key is valid — return your response return Response.json({ message: "Hello from protected endpoint!", keyId: data.keyId, identity: data.identity, }); } catch (err) { console.error(err); return Response.json({ error: "Verification failed" }, { status: 500 }); } }, port: 3000, }); console.log(`Server running at http://localhost:${server.port}`); ``` ```bash theme={null} bun run index.ts ``` Create a test key in your [Unkey dashboard](https://app.unkey.com), then: ```bash Test with valid key theme={null} curl http://localhost:3000 \ -H "Authorization: Bearer YOUR_API_KEY" ``` You should see: ```json theme={null} { "message": "Hello from protected endpoint!", "keyId": "key_...", "identity": null } ``` Try without a key: ```bash Test without key theme={null} curl http://localhost:3000 ``` You'll get: ```json theme={null} { "error": "Missing API key" } ``` ## What's in `data`? After successful verification: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ## Adding routes Bun's built-in server uses a single `fetch` handler. For multiple routes, pattern match on the URL: ```ts index.ts theme={null} import { verifyKey } from "@unkey/api"; // Helper to verify key async function authenticate(req: Request) { const key = req.headers.get("Authorization")?.replace("Bearer ", ""); if (!key) return { valid: false, error: "Missing API key" }; try { const { meta, data } = await verifyKey({ key }); return data; } catch (err) { console.error(err); return { valid: false, error: "Verification failed" }; } } const server = Bun.serve({ async fetch(req) { const url = new URL(req.url); // Public route if (url.pathname === "/") { return Response.json({ message: "Welcome! Try /api/secret" }); } // Protected routes if (url.pathname.startsWith("/api/")) { const auth = await authenticate(req); if (!auth.valid) { return Response.json({ error: auth.error || "Unauthorized" }, { status: 401 }); } // Route to specific handlers if (url.pathname === "/api/secret") { return Response.json({ secret: "data", keyId: auth.keyId }); } if (url.pathname === "/api/user") { return Response.json({ user: auth.identity }); } } return Response.json({ error: "Not found" }, { status: 404 }); }, port: 3000, }); ``` ## Next steps Protect endpoints from abuse Cap total requests per key Fine-grained access control Full TypeScript SDK docs ## Troubleshooting * Ensure the key hasn't expired or been revoked * Verify the header format: `Authorization: Bearer YOUR_KEY` (note the space) Bun automatically loads `.env` files. Make sure: * The `.env` file is in your project root * You're using `Bun.env.VAR_NAME` not `process.env.VAR_NAME` * Restart the server after changing `.env` Run `bun init -y` to ensure you have a proper `tsconfig.json`. Bun handles TypeScript natively, no extra setup needed. # Express Source: https://unkey.com/docs/quickstart/apis/express Protect your Express API routes with Unkey ## What you'll build An Express server with a protected `/secret` route that requires a valid API key. Requests without a valid key get rejected with a 401. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [API created](https://app.unkey.com/apis) in your Unkey dashboard * Node.js 18+ Clone the complete example and run it locally. ```bash theme={null} mkdir unkey-express && cd unkey-express npm init -y npm install express @unkey/api dotenv ``` Get a root key from [Settings → Root Keys](https://app.unkey.com/settings/root-keys) and create a `.env` file: ```bash .env theme={null} UNKEY_ROOT_KEY="unkey_..." ``` Never commit your `.env` file. Add it to `.gitignore`. Create `index.js` with a protected route: ```js index.js theme={null} const express = require("express"); const { verifyKey } = require("@unkey/api"); require("dotenv").config(); const app = express(); const port = process.env.PORT || 3000; // Public route app.get("/", (req, res) => { res.json({ message: "Welcome! Try /secret with an API key." }); }); // Protected route app.get("/secret", async (req, res) => { // 1. Extract the key from the Authorization header const authHeader = req.headers.authorization; const key = authHeader?.replace("Bearer ", ""); if (!key) { return res.status(401).json({ error: "Missing API key" }); } // 2. Verify with Unkey try { const { meta, data } = await verifyKey({ key }); if (!data.valid) { // Key is invalid, expired, rate limited, etc. return res.status(401).json({ error: "Invalid API key", code: data.code }); } // 3. Key is valid — return protected data res.json({ message: "Welcome to the secret route!", keyId: data.keyId, // Include any metadata you attached to the key identity: data.identity, }); } catch (err) { console.error(err); return res.status(500).json({ error: "Could not verify key" }); } }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); }); ``` ```bash theme={null} node index.js ``` First, create a test key in your [Unkey dashboard](https://app.unkey.com), then: ```bash Test with valid key theme={null} curl http://localhost:3000/secret \ -H "Authorization: Bearer YOUR_API_KEY" ``` You should see: ```json theme={null} { "message": "Welcome to the secret route!", "keyId": "key_...", "identity": null } ``` Now try without a key: ```bash Test without key theme={null} curl http://localhost:3000/secret ``` You'll get: ```json theme={null} { "error": "Missing API key" } ``` ## What's in `data`? After successful verification, `data` contains: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ## Using as middleware For cleaner code, extract verification into middleware: ```js middleware/auth.js theme={null} const { verifyKey } = require("@unkey/api"); async function unkeyAuth(req, res, next) { const key = req.headers.authorization?.replace("Bearer ", ""); if (!key) { return res.status(401).json({ error: "Missing API key" }); } try { const { meta, data } = await verifyKey({ key }); if (!data.valid) { return res.status(401).json({ error: "Invalid API key" }); } // Attach key info to request for use in route handlers req.unkey = data; next(); } catch (err) { console.error(err); return res.status(500).json({ error: "Could not verify key" }); } } module.exports = { unkeyAuth }; ``` Then use it on any route: ```js theme={null} const { unkeyAuth } = require("./middleware/auth"); app.get("/secret", unkeyAuth, (req, res) => { // req.unkey contains the verification result res.json({ message: "Secret data", keyId: req.unkey.keyId }); }); app.get("/another-secret", unkeyAuth, (req, res) => { res.json({ data: "More protected content" }); }); ``` ## Next steps Limit requests per key Cap total requests per key Fine-grained access control Full TypeScript SDK docs ## Troubleshooting * Ensure the key hasn't expired or been revoked * Verify the `Authorization` header format: `Bearer YOUR_KEY` (note the space) * Check that your root key has the `verify_key` permission * Check that `UNKEY_ROOT_KEY` is set correctly in your `.env` * Make sure you're calling `require("dotenv").config()` before using env vars * Check the Unkey dashboard for any service issues The code above uses CommonJS. For TypeScript, install types and use imports: ```bash theme={null} npm install -D typescript @types/express @types/node ``` ```ts theme={null} import express from "express"; import { verifyKey } from "@unkey/api"; ``` # Go Source: https://unkey.com/docs/quickstart/apis/go Protect your Go APIs with Unkey This guide shows how to add API key verification to your Go applications using the official Unkey Go SDK. ## Prerequisites * Go 1.21 or higher * An Unkey account (free at [unkey.com](https://unkey.com)) ## 1. Install the SDK ```bash theme={null} go get github.com/unkeyed/sdks/api/go/v2@latest ``` ## 2. Set up your Unkey credentials 1. Create an API in the [Unkey Dashboard](https://app.unkey.com/apis) 2. Create a root key at [Settings → Root Keys](https://app.unkey.com/settings/root-keys) 3. Copy your API ID (looks like `api_xxxx`) Set your root key as an environment variable: ```bash theme={null} export UNKEY_ROOT_KEY="unkey_xxxx" ``` ## 3. Create middleware Here's how to verify API keys with standard library `net/http`: ```go theme={null} package main import ( "context" "net/http" "os" "strings" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } // AuthMiddleware verifies API keys on incoming requests func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Extract API key from header authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, `{"error": "Missing Authorization header"}`, http.StatusUnauthorized) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") // Verify with Unkey res, err := unkeyClient.Keys.VerifyKey(r.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { http.Error(w, `{"error": "Verification service unavailable"}`, http.StatusServiceUnavailable) return } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { http.Error(w, `{"error": "Invalid API key"}`, http.StatusUnauthorized) return } // Add key info to context for handlers ctx := context.WithValue(r.Context(), "keyId", *result.KeyID) next.ServeHTTP(w, r.WithContext(ctx)) }) } func main() { mux := http.NewServeMux() // Protected route mux.HandleFunc("/api/protected", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"message": "Access granted!"}`)) }) // Wrap with auth middleware http.ListenAndServe(":8080", AuthMiddleware(mux)) } ``` ## 4. Run your server ```bash theme={null} go run main.go ``` Test it with a valid API key: ```bash theme={null} curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/protected ``` ## Using with Gin If you're using the [Gin framework](https://gin-gonic.com/): ```go theme={null} package main import ( "net/http" "os" "strings" "github.com/gin-gonic/gin" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } func UnkeyAuth() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing API key"}) return } apiKey := strings.TrimPrefix(authHeader, "Bearer ") res, err := unkeyClient.Keys.VerifyKey(c.Request.Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": "Verification failed"}) return } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "error": "Invalid API key", "code": string(result.Code), }) return } // Store verification result in context c.Set("unkeyResult", &result) c.Next() } } func main() { r := gin.Default() // Protected routes api := r.Group("/api", UnkeyAuth()) { api.GET("/data", func(c *gin.Context) { result := c.MustGet("unkeyResult").(*components.V2KeysVerifyKeyResponseData) c.JSON(http.StatusOK, gin.H{ "message": "Access granted", "key_id": *result.KeyID, }) }) } r.Run(":8080") } ``` ## Using with Echo For the [Echo framework](https://echo.labstack.com/): ```go theme={null} package main import ( "net/http" "os" "strings" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) var unkeyClient *unkey.Unkey func init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) } func UnkeyAuthMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { authHeader := c.Request().Header.Get("Authorization") if authHeader == "" { return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Missing API key"}) } apiKey := strings.TrimPrefix(authHeader, "Bearer ") res, err := unkeyClient.Keys.VerifyKey(c.Request().Context(), components.V2KeysVerifyKeyRequestBody{ Key: apiKey, }) if err != nil { return c.JSON(http.StatusServiceUnavailable, map[string]string{"error": "Verification failed"}) } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { return c.JSON(http.StatusUnauthorized, map[string]string{ "error": "Invalid API key", "code": string(result.Code), }) } // Store in context c.Set("keyId", *result.KeyID) return next(c) } } } func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Protected route e.GET("/api/protected", func(c echo.Context) error { keyId, ok := c.Get("keyId").(string) if !ok || keyId == "" { return c.JSON(http.StatusInternalServerError, map[string]string{ "error": "Key ID not found in context", }) } return c.JSON(http.StatusOK, map[string]string{ "message": "Access granted", "key_id": keyId, }) }, UnkeyAuthMiddleware()) e.Start(":8080") } ``` ## What's next? Protect your endpoints from abuse Complete Go SDK documentation Add roles and permissions More Go recipes and examples # Hono Source: https://unkey.com/docs/quickstart/apis/hono Protect your Hono API routes with Unkey middleware ## What you'll build A Hono app with API key authentication using the `@unkey/hono` middleware. All routes (or specific ones) require a valid API key. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [API created](https://app.unkey.com/apis) in your Unkey dashboard * Node.js 18+ or Bun Clone the complete example and run it locally. ```bash npm theme={null} npm create hono@latest unkey-hono cd unkey-hono ``` ```bash pnpm theme={null} pnpm create hono@latest unkey-hono cd unkey-hono ``` ```bash bun theme={null} bun create hono@latest unkey-hono cd unkey-hono ``` Choose your preferred runtime (Node.js, Bun, Cloudflare Workers, etc.) ```bash npm theme={null} npm install @unkey/hono ``` ```bash pnpm theme={null} pnpm add @unkey/hono ``` ```bash bun theme={null} bun add @unkey/hono ``` Create a `.env` file: ```bash .env theme={null} UNKEY_ROOT_KEY="unkey_..." ``` The Hono middleware verifies keys directly against your root key. Update `src/index.ts`: ```ts src/index.ts theme={null} import { Hono } from "hono"; import { unkey, UnkeyContext } from "@unkey/hono"; // Type the context so you get autocomplete for c.get("unkey") const app = new Hono<{ Variables: { unkey: UnkeyContext } }>(); // Protect all routes with API key authentication app.use("*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, })); // This route now requires a valid API key app.get("/", (c) => { // Access verification result from context const keyInfo = c.get("unkey"); return c.json({ message: "Hello from protected route!", keyId: keyInfo.keyId, valid: keyInfo.valid, }); }); // Another protected route app.get("/secret", (c) => { const keyInfo = c.get("unkey"); return c.json({ secret: "data", identity: keyInfo.identity, meta: keyInfo.meta, }); }); export default app; ``` ```bash npm theme={null} npm run dev ``` ```bash pnpm theme={null} pnpm dev ``` ```bash bun theme={null} bun run dev ``` Create a test key in your [Unkey dashboard](https://app.unkey.com), then: ```bash Test with valid key theme={null} curl http://localhost:3000 \ -H "Authorization: Bearer YOUR_API_KEY" ``` You should see: ```json theme={null} { "message": "Hello from protected route!", "keyId": "key_...", "valid": true } ``` Without a key, you'll get a 401: ```bash Test without key theme={null} curl http://localhost:3000 ``` ```json theme={null} { "error": "Unauthorized" } ``` ## Protecting specific routes Instead of protecting all routes, you can apply the middleware to specific paths: ```ts src/index.ts theme={null} import { Hono } from "hono"; import { unkey, UnkeyContext } from "@unkey/hono"; const app = new Hono<{ Variables: { unkey: UnkeyContext } }>(); // Public route — no middleware app.get("/", (c) => { return c.json({ message: "Welcome! This route is public." }); }); // Protected routes — apply middleware to /api/* paths only app.use("/api/*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY! })); app.get("/api/secret", (c) => { const keyInfo = c.get("unkey"); return c.json({ secret: "data", keyId: keyInfo.keyId }); }); app.get("/api/user", (c) => { const keyInfo = c.get("unkey"); return c.json({ identity: keyInfo.identity }); }); export default app; ``` ## What's in the context? After verification, `c.get("unkey")` contains: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Named role(s) assigned to the key, each representing a set of permissions | | `permissions` | `string[]?` | List of individual permissions granted to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | ## Middleware options ```ts theme={null} unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, // Required: Your root key // Optional: Custom error handling onError: (c, error) => { console.error("Unkey error:", error); return c.json({ error: "Auth service unavailable" }, 503); }, // Optional: Custom unauthorized response handleInvalidKey: (c, result) => { return c.json({ error: "Invalid API key", code: result.code }, 401); }, }); ``` ## Next steps Limit requests per key Cap total requests per key Fine-grained access control Full middleware documentation ## Troubleshooting * Ensure the key hasn't expired or been revoked * Verify the header format: `Authorization: Bearer YOUR_KEY` * For Node.js: Install `dotenv` and add `import 'dotenv/config'` at the top * For Bun: `.env` is loaded automatically * For Cloudflare Workers: Use `wrangler secret` or `wrangler.toml` Use wrangler secrets for your root key: ```bash theme={null} npx wrangler secret put UNKEY_ROOT_KEY ``` Then access it from your Hono bindings. Add `UNKEY_ROOT_KEY` to your `Bindings` type and read it via `ctx.env.UNKEY_ROOT_KEY` or `env.UNKEY_ROOT_KEY` in your handlers. # Next.js Source: https://unkey.com/docs/quickstart/apis/nextjs Protect your Next.js API routes with Unkey ## What you'll build A Next.js API route that requires a valid API key on every request. Invalid or missing keys get rejected with a 401. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [API created](https://app.unkey.com/apis) in your Unkey dashboard Skip this if you have an existing project. ```bash npm theme={null} npx create-next-app@latest my-api cd my-api ``` ```bash pnpm theme={null} pnpm create next-app@latest my-api cd my-api ``` ```bash bun theme={null} bunx create-next-app my-api cd my-api ``` ```bash npm theme={null} npm install @unkey/nextjs ``` ```bash pnpm theme={null} pnpm add @unkey/nextjs ``` ```bash bun theme={null} bun add @unkey/nextjs ``` Get a root key from *Settings → Root Keys* and add it to your environment: ```bash .env.local theme={null} UNKEY_ROOT_KEY="unkey_..." ``` Never commit your root key. Add `.env.local` to `.gitignore`. Create a new API route that requires authentication: ```ts app/api/protected/route.ts theme={null} import { NextRequestWithUnkeyContext, withUnkey } from "@unkey/nextjs"; export const POST = withUnkey( async (req: NextRequestWithUnkeyContext) => { // The key has already been verified at this point // Access verification details via req.unkey.data // Your API logic here // // req.unkey.data contains all verification details. return Response.json({ message: "Hello!", keyId: req.unkey.data.keyId, // If you set an externalId when creating the key: externalId: req.unkey.data.identity?.externalId, }); }, { rootKey: process.env.UNKEY_ROOT_KEY! } ); ``` The `withUnkey` wrapper handles key extraction, verification, and error responses automatically. Invalid keys never reach your handler. ```bash npm theme={null} npm run dev ``` ```bash pnpm theme={null} pnpm dev ``` ```bash bun theme={null} bun dev ``` First, create a test key in your [Unkey dashboard](https://app.unkey.com), then: ```bash Test with valid key theme={null} curl -X POST http://localhost:3000/api/protected \ -H "Authorization: Bearer YOUR_API_KEY" ``` You should see: ```json theme={null} { "message": "Hello!", "keyId": "key_..." } ``` Now try without a key: ```bash Test without key theme={null} curl -X POST http://localhost:3000/api/protected ``` You'll get a `401 Unauthorized` response. ## What's in `req.unkey`? After verification, `req.unkey.data` contains: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | `data.meta` is your custom key metadata (set via `meta` when creating the key). This is different from the response's top-level `meta` which contains `requestId`. ## Next steps Limit requests per key Cap total requests per key Fine-grained access control Full SDK documentation ## Troubleshooting * Ensure the key hasn't expired or been revoked * Verify the `Authorization` header format: `Bearer YOUR_KEY` (note the space) * Check that your root key has the `verify_key` permission * Restart your dev server after adding `.env.local` * Make sure the file is in your project root * Check for typos in the variable name # Python Source: https://unkey.com/docs/quickstart/apis/python Protect your Python APIs with Unkey This guide shows how to add API key verification to your Python applications using the official Unkey Python SDK (v2). ## Prerequisites * Python 3.9 or higher * An Unkey account (free at [unkey.com](https://unkey.com)) ## 1. Install the SDK ```bash pip theme={null} pip install unkey-py ``` ```bash poetry theme={null} poetry add unkey-py ``` ```bash uv theme={null} uv add unkey-py ``` ## 2. Set up your Unkey credentials 1. Create an API in the [Unkey Dashboard](https://app.unkey.com/apis) 2. Create a root key at [Settings → Root Keys](https://app.unkey.com/settings/root-keys) Set your root key as an environment variable: ```bash theme={null} export UNKEY_ROOT_KEY="unkey_xxxx" ``` ## 3. FastAPI Integration Here's how to protect your FastAPI endpoints using the v2 SDK: ```python theme={null} from fastapi import FastAPI, Header, HTTPException from unkey_py import Unkey import os app = FastAPI() @app.get("/api/protected") async def protected_route(x_api_key: str = Header(..., alias="X-API-Key")): with Unkey(bearer_auth=os.environ["UNKEY_ROOT_KEY"]) as unkey: res = unkey.keys.verify_key(request={"key": x_api_key}) result = res.data if not result.valid: raise HTTPException( status_code=401 if result.code == "NOT_FOUND" else 403, detail=f"Unauthorized: {result.code}" ) return { "message": "Access granted", "key_id": result.key_id, "remaining_credits": result.remaining } ``` Run your FastAPI app: ```bash theme={null} uvicorn main:app --reload ``` Test with a valid API key: ```bash theme={null} curl -H "X-API-Key: YOUR_API_KEY" http://localhost:8000/api/protected ``` ## 4. Flask Integration For Flask applications using the v2 SDK: ```python theme={null} from flask import Flask, request, jsonify from unkey_py import Unkey import os app = Flask(__name__) @app.route("/api/protected") def protected_route(): api_key = request.headers.get("X-API-Key") if not api_key: return jsonify({"error": "Missing API key"}), 401 with Unkey(bearer_auth=os.environ["UNKEY_ROOT_KEY"]) as unkey: res = unkey.keys.verify_key(request={"key": api_key}) result = res.data if not result.valid: return jsonify({"error": result.code}), 401 return jsonify({ "message": "Access granted", "key_id": result.key_id, "meta": result.meta }) if __name__ == "__main__": app.run(debug=True) ``` ## 5. Django Integration For Django, create a custom middleware using the v2 SDK: ```python theme={null} # middleware/unkey_auth.py from django.http import JsonResponse from django.conf import settings from unkey_py import Unkey class UnkeyAuthMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # Skip auth for unprotected paths if request.path.startswith('/admin/'): return self.get_response(request) api_key = request.headers.get('X-API-Key') if not api_key: return JsonResponse({'error': 'Missing API key'}, status=401) with Unkey(bearer_auth=settings.UNKEY_ROOT_KEY) as unkey: res = unkey.keys.verify_key(request={'key': api_key}) result = res.data if not result.valid: return JsonResponse({'error': result.code}, status=401) # Attach key info to request request.unkey_key_id = result.key_id request.unkey_meta = result.meta return self.get_response(request) # settings.py import os MIDDLEWARE = [ # ... other middleware 'myapp.middleware.unkey_auth.UnkeyAuthMiddleware', ] UNKEY_ROOT_KEY = os.environ.get("UNKEY_ROOT_KEY") ``` Then in your views: ```python theme={null} from django.http import JsonResponse def protected_view(request): return JsonResponse({ "message": "Access granted", "key_id": request.unkey_key_id, }) ``` ## Creating and Managing Keys Here's how to create API keys programmatically using the v2 SDK: ```python theme={null} from datetime import datetime, timedelta from unkey_py import Unkey import os with Unkey(bearer_auth=os.environ["UNKEY_ROOT_KEY"]) as unkey: # Create a new API key res = unkey.keys.create_key(request={ "api_id": "api_...", "prefix": "sk_live", "external_id": "user_123", # Link to your user "name": "Production key", "expires": int((datetime.now() + timedelta(days=30)).timestamp() * 1000), "remaining": 1000, "refill": { "amount": 1000, "interval": "monthly", }, "ratelimits": [{ "name": "requests", "limit": 100, "duration": 60000, # 100 per minute "auto_apply": True, }], "meta": { "plan": "pro", "customer_id": "cust_123", }, }) result = res.data print(f"Created key: {result.key}") # Only time you'll see it! print(f"Key ID: {result.key_id}") ``` The full API key is only returned once at creation. Store it securely and never show it again. ## Error Handling Always handle Unkey errors gracefully: ```python theme={null} from unkey_py import Unkey import os with Unkey(bearer_auth=os.environ["UNKEY_ROOT_KEY"]) as unkey: try: res = unkey.keys.create_key(request={ "api_id": "api_...", "name": "My Key" }) result = res.data print(f"Created: {result.key}") except Exception as e: print(f"API Error: {e}") # Handle specific errors based on exception type ``` ## What's next? Protect your endpoints from abuse Complete Python SDK documentation Add roles and permissions More Python recipes and examples # Shared Rate Limits Source: https://unkey.com/docs/quickstart/identities/shared-ratelimits Create your first identity and key with Unkey This quickstart will guide you through creating your first identity with shared ratelimits and a key that is connected to the identity. The example is written in TypeScript, purposefully using the `fetch` API to make requests as transparent as possible. You can use any language or library to make requests to the Unkey API. ### Requirements You will need your api id and root key to make requests to the Unkey API. You can find these in the Unkey dashboard. ```ts theme={null} const apiId = "api_XXX"; const rootKey = "unkey_XXX"; ``` The root key requires the following permissions: ```ts theme={null} "identity.*.create_identity" "identity.*.read_identity" "identity.*.update_identity" "api.*.create_key" ``` ### Create an Identity To create an identity, you need to make a request to the `/v2/identities.createIdentity` endpoint. You can specify an `externalId` and `meta` object to store additional information about the identity. Unkey does not care what the `externalId` is, but it must be unique for each identity. Commonly used are user or organization ids. The `meta` object can be used to store any additional information you want to associate with the identity. ```ts theme={null} const externalId = "user_1234abc"; const createIdentityResponse = await fetch("https://api.unkey.com/v2/identities.createIdentity", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${rootKey}`, }, body: JSON.stringify({ externalId, meta: { stripeCustomerId: "cus_123", }, }), }); const { identityId } = await createIdentityResponse.json<{ identityId: string; }>(); ``` ### Retrieve an Identity Let's retrieve the identity to make sure it got created successfully ```ts theme={null} const getIdentityResponse = await fetch(`https://api.unkey.com/v2/identities.getIdentity`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${rootKey}`, }, body: JSON.stringify({ identity: identityId, }), }); const identity = await getIdentityResponse.json<{ id: string; externalId: string; meta: unknown; ratelimits: Array<{ name: string; limit: number; duration: number }>; }>(); ``` ### Create a Key Let's create a key and connect it to the identity ```ts theme={null} const createKeyResponse = await fetch(`https://api.unkey.com/v2/keys.createKey`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${rootKey}`, }, body: JSON.stringify({ apiId: apiId, prefix: "acme", // by providing the same externalId as the identity, we connect the key to the identity externalId: externalId, }), }); const key = await createKeyResponse.json<{ keyId: string; key: string; }>(); ``` ### Verify the Key When you verify the key, you will receive the identity that the key is connected to and can act accordingly in your API handler. ```ts theme={null} const verifyKeyResponse = await fetch(`https://api.unkey.com/v2/keys.verifyKey`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ apiId: apiId, key: key.key, }), }); const verified = await verifyKeyResponse.json<{ valid: boolean; identity: { id: string; externalId: string; meta: unknown; }; }>(); ``` ### Ratelimits Ratelimits can be set on the identity level. Ratelimits set on the identity level are shared across all keys connected to the identity. ```ts theme={null} const updateRes = await fetch("https://api.unkey.com/v2/identities.updateIdentity", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${rootKey}`, }, body: JSON.stringify({ identity: identity.id, ratelimits: [ /** * We define a limit that allows 10 requests per day */ { name: "requests", limit: 10, duration: 24 * 60 * 60 * 1000, // 24h }, /** * And a second limit that allows 1000 tokens per minute */ { name: "tokens", limit: 1000, duration: 60 * 1000, // 1 minute }, ], }), }); ``` ### Verify the Key with Ratelimits Now let's verify the key again and specify the limits In this case, we pretend like a user is requesting to use 200 tokens. We specify the `requests` ratelimit to enforce a limit of 10 requests per day and the `tokens` ratelimit to enforce a limit of 1000 tokens per minute. Additionally we specify the cost of the tokens to be 200. ```ts theme={null} const verifiedWithRatelimitsResponse = await fetch(`https://api.unkey.com/v2/keys.verifyKey`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ apiId: apiId, key: key.key, ratelimits: [ { name: "requests", }, { name: "tokens", cost: 200, }, ], }), }); const verifiedWithRatelimits = await verifiedWithRatelimitsResponse.json<{ valid: boolean; identity: { id: string; externalId: string; meta: unknown; }; }>(); ``` That's it, you have successfully created an identity and key with shared ratelimits. You can now use the key to verify requests and enforce ratelimits in your API handler. # Quickstart Source: https://unkey.com/docs/quickstart/quickstart Verify your first API key in under 5 minutes This guide gets you from zero to a working API key verification as fast as possible. We'll create a key, then verify it using your preferred method. Need an account? [Sign up free](https://app.unkey.com) — takes 30 seconds. ## 1. Create an API An API in Unkey is a container for your keys. Head to your [dashboard](https://app.unkey.com/apis) and create one, or use one you already have. Copy your **API ID** — it looks like `api_xxxx`. ## 2. Create a root key Root keys authenticate *your* requests to the Unkey API (for creating and managing keys). 1. Go to Workspace Settings → Root Keys 2. Click **Create New Root Key** 3. Give it a name and select the permissions you need 4. Copy the key — you won't see it again Keep your root key secret. Never expose it in client-side code or commit it to git. ## 3. Create an API key Now let's create a key that your users would use to authenticate: ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.createKey \ -H "Authorization: Bearer YOUR_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "apiId": "api_xxxx", "name": "My First Key" }' ``` ```typescript TypeScript theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY }); try { const { meta, data } = await unkey.keys.create({ apiId: "api_xxxx", name: "My First Key", }); console.log(data.key); // This is the key to give to your user } catch (err) { console.error(err); } ``` ```python Python theme={null} from unkey import Unkey unkey = Unkey(root_key="your_root_key") result = unkey.keys.create_key( api_id="api_xxxx", name="My First Key" ) print(result.key) # This is the key to give to your user ``` ```go Go theme={null} package main import ( "context" "fmt" "os" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) func main() { ctx := context.Background() client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) res, err := client.Keys.CreateKey(ctx, components.V2KeysCreateKeyRequestBody{ APIID: "api_xxxx", Name: stringPtr("My First Key"), }) if err != nil { panic(err) } fmt.Println(res.V2KeysCreateKeyResponseBody.Key) // Give this to your user } func stringPtr(s string) *string { return &s } ``` Save the returned `key` value — that's what you'll verify in the next step. ## 4. Verify the key This is what you'll do on every API request to check if a key is valid: ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/keys.verifyKey \ -H "Authorization: Bearer YOUR_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "key": "THE_KEY_FROM_STEP_3" }' ``` ```typescript TypeScript theme={null} import { verifyKey } from "@unkey/api"; try { const { meta, data } = await verifyKey({ key: "THE_KEY_FROM_STEP_3", }); if (!data.valid) { // Key is invalid, expired, rate limited, etc. console.log("Denied:", data.code); } // Key is valid — continue with your API logic console.log("Key ID:", data.keyId); } catch (err) { console.error(err); } ``` ```python Python theme={null} from unkey import Unkey unkey = Unkey(root_key="your_root_key") result = unkey.keys.verify_key(key="THE_KEY_FROM_STEP_3") if not result.valid: print("Denied:", result.code) else: print("Key ID:", result.key_id) ``` ```go Go theme={null} package main import ( "context" "fmt" "os" unkey "github.com/unkeyed/sdks/api/go/v2" "github.com/unkeyed/sdks/api/go/v2/models/components" ) func main() { ctx := context.Background() client := unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), ) res, err := client.Keys.VerifyKey(ctx, components.V2KeysVerifyKeyRequestBody{ Key: "THE_KEY_FROM_STEP_3", }) if err != nil { panic(err) } result := res.V2KeysVerifyKeyResponseBody.Data if !result.Valid { code := "unknown" if result.Code != "" { code = string(result.Code) } fmt.Println("Denied:", code) return } fmt.Println("Key ID:", result.KeyId) } ``` A successful response looks like: ```json theme={null} { "meta": { "requestId": "req_..." }, "data": { "valid": true, "code": "VALID", "keyId": "key_xxxx" } } ``` **That's it!** You've just verified an API key with Unkey. 🎉 ## What's in the verification response? The `data` object contains everything you need to make authorization decisions: | Field | Type | Description | | ------------- | ----------- | ------------------------------------------------------------------- | | `valid` | `boolean` | Whether the key passed all checks | | `code` | `string` | Status code (`VALID`, `NOT_FOUND`, `RATE_LIMITED`, etc.) | | `keyId` | `string` | The key's unique identifier | | `name` | `string?` | Human-readable name of the key | | `meta` | `object?` | Custom metadata associated with the key | | `expires` | `number?` | Unix timestamp (in milliseconds) when the key will expire. (if set) | | `credits` | `number?` | Remaining uses (if usage limits set) | | `enabled` | `boolean` | Whether the key is enabled | | `roles` | `string[]?` | Permissions attached to the key | | `permissions` | `string[]?` | Permissions attached to the key | | `identity` | `object?` | Identity info if `externalId` was set when creating the key | | `ratelimits` | `object[]?` | Rate limit states (if rate limiting configured) | Fields marked with `?` are optional and only included when relevant (e.g., `remaining` only appears if you set a usage limit). ## Next steps Now integrate Unkey into your actual application: Protect API routes with middleware Add key verification to Express routes Fast verification with Bun's native server Edge-ready API key verification Or explore more features: Protect endpoints from abuse Cap requests per key for billing tiers Fine-grained access control Full TypeScript SDK docs # Bun Source: https://unkey.com/docs/quickstart/ratelimiting/bun Add rate limiting to your Bun server ## What you'll build A Bun HTTP server with rate limiting. Users who exceed the limit get a 429 response. **Time to complete:** \~3 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission * [Bun](https://bun.sh) installed ```bash theme={null} mkdir unkey-bun-ratelimit && cd unkey-bun-ratelimit bun init -y ``` ```bash theme={null} bun add @unkey/ratelimit ``` Create a `.env` file: ```bash .env theme={null} UNKEY_ROOT_KEY="unkey_..." ``` Replace `index.ts`: ```ts index.ts theme={null} import { Ratelimit } from "@unkey/ratelimit"; // Create limiter instance const limiter = new Ratelimit({ rootKey: Bun.env.UNKEY_ROOT_KEY!, namespace: "bun-api", limit: 10, // 10 requests... duration: "60s", // ...per minute }); const server = Bun.serve({ async fetch(req) { const url = new URL(req.url); // Public route if (url.pathname === "/") { return Response.json({ message: "Welcome! Try /api/data" }); } // Rate-limited route if (url.pathname === "/api/data") { // 1. Identify the user const identifier = req.headers.get("x-user-id") ?? req.headers.get("x-forwarded-for") ?? "anonymous"; // 2. Check the rate limit const { success, remaining, reset } = await limiter.limit(identifier); // 3. Set headers const headers = { "X-RateLimit-Limit": "10", "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }; if (!success) { return Response.json( { error: "Too many requests. Try again later." }, { status: 429, headers } ); } // 4. Request allowed return Response.json( { message: "Here's your data!", remaining }, { headers } ); } return Response.json({ error: "Not found" }, { status: 404 }); }, port: 3000, }); console.log(`Server running at http://localhost:${server.port}`); ``` ```bash theme={null} bun run index.ts ``` ```bash theme={null} # Hit the endpoint 12 times for i in {1..12}; do curl http://localhost:3000/api/data -H "x-user-id: test-user" echo "" done ``` First 10 requests return data. Requests 11+ get: ```json theme={null} { "error": "Too many requests. Try again later." } ``` ## What's in the response? `limiter.limit()` returns: | Field | Type | Description | | ----------- | --------- | ------------------------------------------ | | `success` | `boolean` | `true` if allowed, `false` if rate limited | | `remaining` | `number` | Requests left in current window | | `reset` | `number` | Unix timestamp (ms) when window resets | | `limit` | `number` | The configured limit | ## Multiple rate limiters Create different limiters for different use cases: ```ts index.ts theme={null} import { Ratelimit } from "@unkey/ratelimit"; // General API: 100/min const apiLimiter = new Ratelimit({ rootKey: Bun.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "60s", }); // Auth endpoints: 5/min (prevent brute force) const authLimiter = new Ratelimit({ rootKey: Bun.env.UNKEY_ROOT_KEY!, namespace: "auth", limit: 5, duration: "60s", }); // Helper to apply rate limiting async function checkLimit(limiter: Ratelimit, identifier: string) { const result = await limiter.limit(identifier); if (!result.success) { return Response.json( { error: "Rate limit exceeded" }, { status: 429 } ); } return null; // Allowed } const server = Bun.serve({ async fetch(req) { const url = new URL(req.url); const ip = req.headers.get("x-forwarded-for") ?? "unknown"; if (url.pathname === "/api/login") { const blocked = await checkLimit(authLimiter, ip); if (blocked) return blocked; return Response.json({ message: "Login endpoint" }); } if (url.pathname.startsWith("/api/")) { const blocked = await checkLimit(apiLimiter, ip); if (blocked) return blocked; return Response.json({ message: "API data" }); } return Response.json({ message: "Welcome" }); }, port: 3000, }); ``` ## Next steps Understand the architecture Give specific users higher limits All configuration options Combine with API key authentication ## Troubleshooting * Verify `UNKEY_ROOT_KEY` is in your `.env` file * Use `Bun.env.UNKEY_ROOT_KEY` (not `process.env`) * Check your root key has `ratelimit.*.limit` permission Bun loads `.env` automatically. Make sure the file is in your project root and restart the server. # Express Source: https://unkey.com/docs/quickstart/ratelimiting/express Add rate limiting to your Express API routes ## What you'll build An Express server with rate-limited endpoints. Users who exceed the limit get a 429 response. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission * Node.js 18+ ```bash theme={null} mkdir unkey-express-ratelimit && cd unkey-express-ratelimit npm init -y npm install express @unkey/ratelimit dotenv ``` Create a `.env` file: ```bash .env theme={null} UNKEY_ROOT_KEY="unkey_..." ``` Never commit your `.env` file. Add it to `.gitignore`. Create `index.js`: ```js index.js theme={null} const express = require("express"); const { Ratelimit } = require("@unkey/ratelimit"); require("dotenv").config(); const app = express(); const port = process.env.PORT || 3000; // Create limiter instance const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "express-api", limit: 10, // 10 requests... duration: "60s", // ...per minute }); // Public route app.get("/", (req, res) => { res.json({ message: "Welcome! Try /api/data" }); }); // Rate-limited route app.get("/api/data", async (req, res) => { // 1. Identify the user (IP, user ID, API key, etc.) const identifier = req.headers["x-user-id"] || req.ip || "anonymous"; // 2. Check the rate limit const { success, remaining, reset } = await limiter.limit(identifier); // 3. Set rate limit headers res.set({ "X-RateLimit-Limit": "10", "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }); if (!success) { return res.status(429).json({ error: "Too many requests. Please try again later.", retryAfter: Math.ceil((reset - Date.now()) / 1000), }); } // 4. Request allowed res.json({ message: "Here's your data!", remaining, }); }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); }); ``` ```bash theme={null} node index.js ``` ```bash theme={null} # Hit the endpoint 12 times for i in {1..12}; do curl http://localhost:3000/api/data -H "x-user-id: test-user" echo "" done ``` First 10 requests succeed. Requests 11+ get: ```json theme={null} { "error": "Too many requests. Please try again later.", "retryAfter": 45 } ``` ## What's in the response? `limiter.limit()` returns: | Field | Type | Description | | ----------- | --------- | ------------------------------------------ | | `success` | `boolean` | `true` if allowed, `false` if rate limited | | `remaining` | `number` | Requests left in current window | | `reset` | `number` | Unix timestamp (ms) when window resets | | `limit` | `number` | The configured limit | ## Using as middleware For cleaner code, create reusable middleware: ```js middleware/ratelimit.js theme={null} const { Ratelimit } = require("@unkey/ratelimit"); const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "api", limit: 100, duration: "1m", }); async function rateLimit(req, res, next) { const identifier = req.headers["x-user-id"] || req.ip || "anonymous"; const { success, remaining, reset } = await limiter.limit(identifier); res.set({ "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }); if (!success) { return res.status(429).json({ error: "Rate limit exceeded" }); } next(); } module.exports = { rateLimit }; ``` ### createMiddleware helper Create an Express middleware from a Ratelimit instance: ```js theme={null} function createMiddleware(limiter) { return async (req, res, next) => { const identifier = req.ip ?? req.headers["x-forwarded-for"] ?? "unknown"; const { success, remaining, reset } = await limiter.limit(identifier); if (!success) { res.set("Retry-After", Math.ceil((reset - Date.now()) / 1000).toString()); res.set("X-RateLimit-Remaining", "0"); return res.status(429).json({ error: "Too many requests" }); } res.set("X-RateLimit-Remaining", remaining.toString()); next(); }; } ``` Use on any route: ```js theme={null} const { rateLimit } = require("./middleware/ratelimit"); app.get("/api/data", rateLimit, (req, res) => { res.json({ data: "protected content" }); }); app.post("/api/submit", rateLimit, (req, res) => { res.json({ success: true }); }); ``` ## Different limits per route Create multiple limiters: ```js theme={null} const apiLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "api", limit: 100, duration: "1m", }); const authLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "auth", limit: 5, duration: "1m", }); // 100/min for general API app.use("/api", createMiddleware(apiLimiter)); // 5/min for login (prevent brute force) app.post("/login", createMiddleware(authLimiter), loginHandler); ``` ## Next steps Understand the architecture Give specific users higher limits All configuration options Combine with API key authentication ## Troubleshooting * Verify `UNKEY_ROOT_KEY` is set and has `ratelimit.*.limit` permission * Make sure you're using a consistent identifier per user * Check that `.env` is loaded before creating the limiter Install types and use imports: ```bash theme={null} npm install -D typescript @types/express @types/node ``` ```ts theme={null} import express from "express"; import { Ratelimit } from "@unkey/ratelimit"; ``` # Hono Source: https://unkey.com/docs/quickstart/ratelimiting/hono Add rate limiting to your Hono API routes ## What you'll build A Hono app with rate-limited endpoints. Users who exceed the limit get a 429 response. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission * Node.js 18+ or Bun ```bash npm theme={null} npm create hono@latest unkey-hono-ratelimit cd unkey-hono-ratelimit ``` ```bash pnpm theme={null} pnpm create hono@latest unkey-hono-ratelimit cd unkey-hono-ratelimit ``` ```bash bun theme={null} bun create hono@latest unkey-hono-ratelimit cd unkey-hono-ratelimit ``` Choose your preferred runtime. ```bash npm theme={null} npm install @unkey/ratelimit ``` ```bash pnpm theme={null} pnpm add @unkey/ratelimit ``` ```bash bun theme={null} bun add @unkey/ratelimit ``` Create a `.env` file: ```bash .env theme={null} UNKEY_ROOT_KEY="unkey_..." ``` Update `src/index.ts`: ```ts src/index.ts theme={null} import { Hono } from "hono"; import { Ratelimit } from "@unkey/ratelimit"; const app = new Hono(); // Create limiter instance const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "hono-api", limit: 10, // 10 requests... duration: "60s", // ...per minute }); // Public route app.get("/", (c) => { return c.json({ message: "Welcome! Try /api/data" }); }); // Rate-limited route app.get("/api/data", async (c) => { // 1. Identify the user const identifier = c.req.header("x-user-id") ?? c.req.header("x-forwarded-for") ?? "anonymous"; // 2. Check the rate limit const { success, remaining, reset } = await limiter.limit(identifier); // 3. Set headers c.header("X-RateLimit-Limit", "10"); c.header("X-RateLimit-Remaining", remaining.toString()); c.header("X-RateLimit-Reset", reset.toString()); if (!success) { return c.json( { error: "Too many requests. Try again later." }, 429 ); } // 4. Request allowed return c.json({ message: "Here's your data!", remaining }); }); export default app; ``` ```bash npm theme={null} npm run dev ``` ```bash pnpm theme={null} pnpm dev ``` ```bash bun theme={null} bun dev ``` ```bash theme={null} # Hit the endpoint 12 times for i in {1..12}; do curl http://localhost:3000/api/data -H "x-user-id: test-user" echo "" done ``` First 10 requests succeed. Requests 11+ get: ```json theme={null} { "error": "Too many requests. Try again later." } ``` ## What's in the response? `limiter.limit()` returns: | Field | Type | Description | | ----------- | --------- | ------------------------------------------ | | `success` | `boolean` | `true` if allowed, `false` if rate limited | | `remaining` | `number` | Requests left in current window | | `reset` | `number` | Unix timestamp (ms) when window resets | | `limit` | `number` | The configured limit | ## Using as middleware Create reusable middleware for cleaner code: ```ts src/middleware/ratelimit.ts theme={null} import { Context, Next } from "hono"; import { Ratelimit } from "@unkey/ratelimit"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "60s", }); export async function rateLimit(c: Context, next: Next) { const identifier = c.req.header("x-user-id") ?? c.req.header("x-forwarded-for") ?? "anonymous"; const { success, remaining, reset } = await limiter.limit(identifier); c.header("X-RateLimit-Remaining", remaining.toString()); c.header("X-RateLimit-Reset", reset.toString()); if (!success) { return c.json({ error: "Rate limit exceeded" }, 429); } await next(); } ``` Apply to routes: ```ts src/index.ts theme={null} import { Hono } from "hono"; import { rateLimit } from "./middleware/ratelimit"; const app = new Hono(); // Public app.get("/", (c) => c.json({ message: "Welcome" })); // Rate-limited routes app.use("/api/*", rateLimit); app.get("/api/data", (c) => { return c.json({ data: "protected content" }); }); app.get("/api/user", (c) => { return c.json({ user: "info" }); }); export default app; ``` ## Different limits per route group ```ts theme={null} const apiLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "60s", }); const authLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "auth", limit: 5, duration: "60s", }); // Apply different limiters to different route groups app.use("/api/*", createRateLimitMiddleware(apiLimiter)); app.use("/auth/*", createRateLimitMiddleware(authLimiter)); ``` ## Deploying to Cloudflare Workers For Cloudflare Workers, access env through the context: ```ts theme={null} app.get("/api/data", async (c) => { const limiter = new Ratelimit({ rootKey: c.env.UNKEY_ROOT_KEY, // Access from c.env namespace: "api", limit: 10, duration: "60s", }); const { success } = await limiter.limit("user-id"); // ... }); ``` Set your secret with wrangler: ```bash theme={null} npx wrangler secret put UNKEY_ROOT_KEY ``` ## Next steps Understand the architecture Give specific users higher limits All configuration options Combine with API key authentication ## Troubleshooting * Verify `UNKEY_ROOT_KEY` is set correctly * For Workers: use `c.env.UNKEY_ROOT_KEY`, not `process.env` * Check your root key has `ratelimit.*.limit` permission * For Node.js: Install `dotenv` and add `import 'dotenv/config'` * For Bun: `.env` loads automatically * Restart the dev server after changes # Next.js Source: https://unkey.com/docs/quickstart/ratelimiting/nextjs Add rate limiting to your Next.js API routes ## What you'll build A Next.js API route that limits each user to a set number of requests per time window. Excess requests get rejected with a 429. **Time to complete:** \~5 minutes ## Prerequisites * [Unkey account](https://app.unkey.com/auth/sign-up) (free) * [Root key](https://app.unkey.com/settings/root-keys) with `ratelimit.*.limit` permission * Node.js 18+ Skip if you have an existing project. ```bash npm theme={null} npx create-next-app@latest my-app cd my-app ``` ```bash pnpm theme={null} pnpm create next-app@latest my-app cd my-app ``` ```bash bun theme={null} bunx create-next-app my-app cd my-app ``` ```bash npm theme={null} npm install @unkey/ratelimit ``` ```bash pnpm theme={null} pnpm add @unkey/ratelimit ``` ```bash bun theme={null} bun add @unkey/ratelimit ``` Create or update `.env.local`: ```bash .env.local theme={null} UNKEY_ROOT_KEY="unkey_..." ``` Never commit your root key. Add `.env.local` to `.gitignore`. ```ts app/api/protected/route.ts theme={null} import { NextResponse } from "next/server"; import { Ratelimit } from "@unkey/ratelimit"; // Create limiter instance outside the handler const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "my-app", // Group related limits limit: 10, // 10 requests... duration: "60s", // ...per minute }); export async function POST(req: Request) { // 1. Identify the user (IP, user ID, API key, etc.) const identifier = req.headers.get("x-user-id") ?? req.headers.get("x-forwarded-for") ?? "anonymous"; // 2. Check the rate limit const { success, remaining, reset } = await limiter.limit(identifier); // 3. Add rate limit headers (optional but nice for clients) const headers = { "X-RateLimit-Limit": "10", "X-RateLimit-Remaining": remaining.toString(), "X-RateLimit-Reset": reset.toString(), }; if (!success) { return NextResponse.json( { error: "Too many requests. Please try again later." }, { status: 429, headers } ); } // 4. Request allowed — do your thing return NextResponse.json( { message: "Hello!", remaining }, { headers } ); } ``` ```bash npm theme={null} npm run dev ``` ```bash pnpm theme={null} pnpm dev ``` ```bash bun theme={null} bun dev ``` ```bash theme={null} # Hit the endpoint multiple times for i in {1..12}; do curl -X POST http://localhost:3000/api/protected \ -H "x-user-id: test-user" echo "" done ``` First 10 requests return `200`. Requests 11+ return `429`: ```json theme={null} { "error": "Too many requests. Please try again later." } ``` Wait 60 seconds and the limit resets. ## What's in the response? `limiter.limit()` returns: | Field | Type | Description | | ----------- | --------- | ----------------------------------------------------- | | `success` | `boolean` | `true` if request is allowed, `false` if rate limited | | `remaining` | `number` | Requests remaining in current window | | `reset` | `number` | Unix timestamp (ms) when the window resets | | `limit` | `number` | The configured limit | ## Choosing an identifier The identifier determines *who* gets rate limited. Common choices: | Identifier | Use case | Example | | ---------- | -------------------------- | ------------------------------------ | | User ID | Authenticated users | `req.auth.userId` | | API key | Per-key limits | `req.headers.get("x-api-key")` | | IP address | Anonymous/public endpoints | `req.headers.get("x-forwarded-for")` | | Combo | Extra specificity | `${userId}:${endpoint}` | ## Creating a reusable limiter For cleaner code, create a utility: ```ts lib/ratelimit.ts theme={null} import { Ratelimit } from "@unkey/ratelimit"; export const apiLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "api", limit: 100, duration: "1m", }); export const authLimiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY!, namespace: "auth", limit: 5, duration: "1m", }); ``` Then use in routes: ```ts app/api/login/route.ts theme={null} import { authLimiter } from "@/lib/ratelimit"; export async function POST(req: Request) { const ip = req.headers.get("x-forwarded-for") ?? "unknown"; const { success } = await authLimiter.limit(ip); if (!success) { return Response.json({ error: "Too many login attempts" }, { status: 429 }); } // Handle login... } ``` ## Next steps Understand the rate limiting architecture Give specific users higher limits All configuration options Combine rate limiting with authentication ## Troubleshooting * Check that `UNKEY_ROOT_KEY` is set in `.env.local` * Verify your root key has `ratelimit.*.limit` permission * Make sure you're using the same identifier each request * Restart the dev server after changing `.env.local` * Unkey's SDK retries failed requests automatically * If errors persist, check [status.unkey.com](https://status.unkey.com) * Check [status.unkey.com](https://status.unkey.com) if issues persist Create multiple `Ratelimit` instances with different namespaces and limits. Each namespace tracks limits independently. # Automated Overrides Source: https://unkey.com/docs/ratelimiting/automated-overrides Manage dynamic overrides programmatically Unkey's ratelimit override API allows you to manage dynamic overrides in response to events in your system. For example when your customer upgrades to an enterprise plan, you might want to create overrides for them to give them higher quotas. Let's look at common scenarios and how to implement them using our [@unkey/api SDK](https://www.unkey.com/docs/libraries/ts/sdk/overview). Our application has a ratelimit namespace called `email.send`, which ratelimits users from sending OTP emails during login. As identifier we're using their email address. ## Set Override In this example, we'll set an override for all users of our fictional customer `calendso.com`. How you detect a change is up to you, typically it's either through a user or admin action, or some form of incoming webhook from your billing or auth provider. ```ts theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY!, }); await unkey.ratelimits.setOverride({ namespaceName: "email.send", // set the override for all users with this domain identifier: "*@calendso.com", limit: 10, duration: 60_000, // 1 minute async: true }) ``` [API Reference ->](/api-reference/v2/overview) Now, when we're ratelimiting `tim@calendso.com`, it will use the override settings and ratelimit them to 10 per minute. ## Get Override Retrieve a single override for an identifier within a namespace. ```ts theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env["UNKEY_ROOT_KEY"] ?? "", }); async function run() { const result = await unkey.ratelimit.getOverride({ namespace: "api.requests", identifier: "premium_user_123", }); console.log(result); } run(); { meta: { requestId: "req_123", }, data: { overrideId: "", duration: 949068, identifier: "", limit: 641349, }, } ``` [API Reference ->](/api-reference/v2/overview) ## List Overrides You can list all of the configured overirdes for a namespace to build your own dashboards. ```ts theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env["UNKEY_ROOT_KEY"] ?? "", }); async function run() { const result = await unkey.ratelimit.listOverrides({ namespace: "api.requests", limit: 20, }); console.log(result); } run(); { meta: { requestId: "req_123", }, data: [], pagination: { cursor: "eyJrZXkiOiJrZXlfMTIzNCIsInRzIjoxNjk5Mzc4ODAwfQ==", hasMore: true, }, } ``` [API Reference ->](/api-reference/v2/overview) ## Delete Override Once they downgrade their plan, we can revoke any overrides: ```ts theme={null} import { Unkey } from "@unkey/api"; const unkey = new Unkey({ rootKey: process.env["UNKEY_ROOT_KEY"] ?? "", }); async function run() { const result = await unkey.ratelimit.deleteOverride({ namespace: "api.requests", identifier: "premium_user_123", }); console.log(result); } run(); { meta: { requestId: "req_123", }, data: {}, } ``` [API Reference ->](/api-reference/v2/overview) # Overview Source: https://unkey.com/docs/ratelimiting/introduction Protect any endpoint from abuse — no Redis required Rate limiting controls how many requests a user, IP, or any identifier can make in a given time window. Unkey provides globally distributed rate limiting that works at the edge — without you managing Redis, Upstash, or any infrastructure. ## When to use rate limiting Stop bad actors from hammering your endpoints or scraping your data. Limit expensive operations (AI calls, database queries) before they blow up your bill. Ensure no single user monopolizes shared resources. Enforce contractual limits (e.g., 10,000 requests/month on a Basic plan). ## How it works Decide what you're limiting: a user ID, API key, IP address, organization, or any string that uniquely identifies the requester. Define how many requests are allowed and over what duration. Example: 100 requests per minute. Call `limiter.limit(identifier)` and Unkey tells you whether to allow or reject the request. ## Quick example ```ts TypeScript theme={null} import { Ratelimit } from "@unkey/ratelimit"; const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "my-app", // Group related limits together limit: 10, // 10 requests... duration: "60s", // ...per minute }); export async function handler(req: Request) { // Use any identifier: user ID, API key, IP, etc. const identifier = req.headers.get("x-user-id") ?? getClientIP(req); const { success, remaining, reset } = await limiter.limit(identifier); if (!success) { return new Response("Too many requests", { status: 429, headers: { "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": reset.toString(), } }); } // Request allowed — continue with your logic return new Response(`Hello! ${remaining} requests remaining.`); } ``` ```bash cURL theme={null} curl -X POST https://api.unkey.com/v2/ratelimits.limit \ -H "Authorization: Bearer unkey_..." \ -H "Content-Type: application/json" \ -d '{ "namespace": "my-app", "identifier": "user_123", "limit": 10, "duration": 60000 }' ``` ## Standalone vs Key-attached rate limits Unkey offers two ways to rate limit: | Approach | Best for | How it works | | ---------------- | ------------------------------- | --------------------------------------------------------------------- | | **Standalone** | Any endpoint, public or private | You call `limiter.limit()` with any identifier | | **Key-attached** | API key authenticated endpoints | Rate limits are configured per-key and checked during `keys.verify()` | **Standalone** is what this section covers — it works anywhere, with or without API keys. **Key-attached** rate limits are configured when you [create API keys](/apis/features/ratelimiting/overview) and are automatically enforced during verification. You can use both! Standalone for public endpoints (login, signup), key-attached for authenticated API calls. ## What makes Unkey rate limiting different? No Redis clusters, no Upstash accounts, no connection strings. Just install the SDK and go. Requests are processed across our globally distributed infrastructure. Your rate limits are checked close to your users, not in a single region. See real-time performance metrics at [ratelimit.unkey.com](https://ratelimit.unkey.com) — our global latency and throughput benchmarks updated live. Configure custom timeout and fallback behavior for resilience when network issues occur. Give specific users higher limits without changing code. "User X gets 1000/min instead of 100/min." See which identifiers are hitting limits, when, and how often — in your Unkey dashboard. ## Get started Go to [Settings → Root Keys](https://app.unkey.com/settings/root-keys) and create a new key with these permissions: * `ratelimit.*.create_namespace` * `ratelimit.*.limit` ```bash theme={null} npm install @unkey/ratelimit ``` See the [framework guides](/quickstart) for complete examples with Next.js, Bun, Express, or Hono. ## Next steps Full walkthrough for your framework. Choose between consistency and speed. Give specific users custom limits. All configuration options and methods. # How Rate Limiting Works Source: https://unkey.com/docs/ratelimiting/modes Understanding Unkey's global rate limiting architecture Unkey's rate limiting is designed for global, low-latency enforcement across distributed systems. ## Architecture When you call `limiter.limit(identifier)`: 1. Request hits the nearest Unkey location 2. Counter is checked and updated 3. Decision returned in \~30ms globally See real-time performance metrics at [ratelimit.unkey.com](https://ratelimit.unkey.com). ```mermaid theme={null} graph LR A[Your API] --> B[Unkey] B --> C[Check Counter] C --> D{Under Limit?} D -->|Yes| E[Allow + Decrement] D -->|No| F[Reject] ``` ## Sliding window algorithm Unkey uses a sliding window algorithm that provides smooth rate limiting without the "burst at window start" problem of fixed windows. **Fixed window problem:** * Limit: 100/minute * User sends 100 requests at 0:59 * Window resets at 1:00 * User sends 100 more at 1:01 * Result: 200 requests in 2 seconds ❌ **Sliding window solution:** * Limit: 100/minute * Considers requests from the past 60 seconds at any point * No burst exploitation possible ## Global consistency Rate limits are enforced consistently across all regions. A user can't bypass limits by hitting different geographic endpoints. ## Response fields Every rate limit check returns: | Field | Type | Description | | ----------- | --------- | -------------------------------------- | | `success` | `boolean` | `true` if request is allowed | | `limit` | `number` | The configured limit | | `remaining` | `number` | Requests left in current window | | `reset` | `number` | Unix timestamp (ms) when window resets | ## Handling the response ```typescript theme={null} const { success, remaining, reset } = await limiter.limit(identifier); if (!success) { // Calculate retry time const retryAfter = Math.ceil((reset - Date.now()) / 1000); return new Response("Rate limit exceeded", { status: 429, headers: { "Retry-After": retryAfter.toString(), "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": reset.toString(), }, }); } // Request allowed ``` ## Cost-based limiting Not all requests are equal. Use `cost` to deduct more from the limit for expensive operations: ```typescript theme={null} // Normal request: costs 1 await limiter.limit(userId); // Expensive operation: costs 5 await limiter.limit(userId, { cost: 5 }); ``` With a limit of 100/minute: * 100 normal requests, OR * 20 expensive requests, OR * Mix of both ## Timeout and fallback Configure behavior when Unkey is unreachable: ```typescript theme={null} const limiter = new Ratelimit({ rootKey: process.env.UNKEY_ROOT_KEY, namespace: "api", limit: 100, duration: "60s", timeout: { ms: 3000, // Wait max 3 seconds fallback: (identifier) => ({ success: true, // Allow on timeout (or false to deny) limit: 0, remaining: 0, reset: Date.now(), }), }, onError: (err, identifier) => { console.error(`Rate limit error for ${identifier}:`, err); return { success: true, limit: 0, remaining: 0, reset: Date.now() }; }, }); ``` ## Next steps Give specific users different limits Full SDK documentation # Custom Overrides Source: https://unkey.com/docs/ratelimiting/overrides Give specific users higher (or lower) rate limits without code changes Override default rate limits for specific identifiers directly from the dashboard. No code changes or deploys needed — changes roll out globally in seconds. ## When to use this Enterprise customers need higher limits than your default tier. Integration partners building on your API need room to grow. Reduce limits for suspicious users without blocking entirely. Temporarily increase limits for specific test accounts. ## How it works 1. You define default limits in your code 2. Create overrides in the dashboard for specific identifiers 3. When that identifier is rate limited, Unkey uses the override instead of the default **Override priority:** Exact matches > Wildcard matches > Default limits ## Create an override Click **Ratelimit** in the sidebar → select your namespace → **Overrides** tab. If you don't have a namespace yet, create one first. New override form Enter: * **Identifier**: The exact identifier or wildcard pattern * **Limit**: Custom request limit * **Duration**: Time window for the limit Click **Override Identifier**. Changes propagate globally within \~60 seconds (usually much faster). ## Example: Enterprise customer Your default limit is 100 requests/minute. Acme Corp needs 10,000/minute. | Identifier | Limit | Duration | | ----------- | ----- | -------- | | `acme-corp` | 10000 | 60s | Now when `acme-corp` hits your API, they get 10,000/min instead of 100/min. ## Wildcard patterns Use `*` to match multiple identifiers at once. ### Examples | Pattern | Matches | | -------------- | ------------------------------------------------------ | | `*@acme.com` | `alice@acme.com`, `bob@acme.com`, `api@acme.com` | | `enterprise:*` | `enterprise:123`, `enterprise:acme`, `enterprise:test` | | `user_*_prod` | `user_123_prod`, `user_abc_prod` | ### Priority Exact matches always win over wildcards: | Override | Limit | | -------------- | --------- | | `*@acme.com` | 500/min | | `ceo@acme.com` | 10000/min | **Result:** * `ceo@acme.com` → 10,000/min (exact match) * `anyone-else@acme.com` → 500/min (wildcard match) * `user@other.com` → default limit Wildcard override example ## Managing overrides via API Create overrides programmatically: ```bash theme={null} curl -X POST https://api.unkey.com/v2/ratelimits.setOverride \ -H "Authorization: Bearer $UNKEY_ROOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "namespaceId": "rl_...", "identifier": "enterprise:acme", "limit": 10000, "duration": 60000 }' ``` This enables workflows like: * Automatically increasing limits when users upgrade * Syncing limits from your billing system * Temporary increases during promotions ## Common patterns ### Tier-based limits ``` free:* → 100/min pro:* → 1000/min enterprise:* → 10000/min ``` Use prefixed identifiers in your code: `${plan}:${userId}` ### Domain-based limits ``` *@bigcustomer.com → 5000/min *@partner.io → 2000/min ``` ### Temporary boost Need to give someone extra capacity for a demo or migration? Add an override, then remove it when done. No code changes needed. ## Removing overrides Delete an override from the dashboard or API. The identifier immediately falls back to default limits (or the next matching wildcard). ## Next steps Understand rate limiting architecture Configure multiple limits per key # Delete Protection Source: https://unkey.com/docs/security/delete-protection Prevents an resource from being deleted when enabled. # Delete Protection Delete Protection is a safety feature that prevents accidental deletion of a resource. When enabled, it blocks all deletion attempts through both the dashboard and API. ## Overview **Delete Protection** adds an extra layer of security by: * Preventing accidental deletion of critical resources * Requiring explicit confirmation to disable protection * Blocking deletion attempts through both UI and API ## Supported Resources Currently, Delete Protection is available for: * APIs: Protect your API configurations and settings from accidental deletion More resources will be added in future updates. ## Enabling API Delete Protection 1. Navigate to your API settings in the dashboard 2. Click "Enable Delete Protection" 3. Type the API name to confirm 4. Click "Enable API Delete Protection" to add protection Delete Protection confirmation dialog showing the Enable API Delete Protection button Once enabled, the API cannot be deleted until protection is explicitly disabled. ## Disabling API Delete Protection 1. Navigate to your API settings in the dashboard 2. Click "Disable Delete Protection" 3. Type the API name to confirm 4. Click "Disable API Delete Protection" to remove protection ## Resource Behavior When Delete Protection is enabled: * All deletion attempts are blocked * The resource returns a `DELETE_PROTECTED` error * The error includes a link to this documentation ### Example API Error Response ```json theme={null} { "error": { "code": "DELETE_PROTECTED", "docs": "https://unkey.com/docs/api-reference/errors/code/DELETE_PROTECTED", "message": "api [apiId] is protected from deletions", "requestId": "req_1234" } } ``` ## Best Practices 1. **Enable for Production resources**: Always enable Delete Protection for resources in production 2. **Use for Critical resources**: Protect resources that contain important data or configurations 3. **Regular Review**: Periodically review protection status of your resources 4. **Team Communication**: Inform team members about protected resources 5. **Documentation**: Document which resources are protected and why ## Limitations * Delete Protection only prevents deletion of the resource * It does not prevent modifications to the resource * It does not prevent deletion of children of a resource, like keys or ratelimits that belong to a protected resource * Protection can be disabled by any user with appropriate permissions ## Related Errors * [DELETE\_PROTECTED](https://www.unkey.com/docs/api-reference/errors/code/DELETE_PROTECTED) - Returned when attempting to delete a protected API # GitHub Secret Scanning Source: https://unkey.com/docs/security/github-scanning How Unkey protects you from leaked root keys Unkey has partnered with GitHub to scan repositories for leaked keys. GitHub Secret Scanning uses regular expressions to scan repositories for keys matching Unkey root keys. If a key is found, GitHub will notify Unkey, and we will validate the key and notify users via email. To ensure the production environment remains up and running, we do not disable the key. This is a service that is automatic and requires nothing from you to function. However, outside of GitHub, we will not be able to inform you if a key has leaked. Learn more: [GitHub Secret Scanning](https://docs.github.com/en/code-security/secret-scanning) ## Sources Scanned * content * commit * pull\_request\_title * pull\_request\_description * pull\_request\_comment * issue\_title * issue\_description * issue\_comment * discussion\_title * discussion\_body * discussion\_comment * commit\_comment * gist\_content * gist\_comment * npm * unknown # Overview Source: https://unkey.com/docs/security/overview How Unkey protects your API keys and data Security is foundational to Unkey. We handle API keys for your production systems, so we take this responsibility seriously. ## Key security principles We hash your API keys before storage. Even if our database were compromised, attackers couldn't recover the original keys. Create, rotate, and revoke keys instantly. Changes propagate globally within seconds. Root keys use explicit permissions. Grant only what's needed for each use case. Every key operation is logged. Know who did what, when, from where. ## How API key storage works When you create an API key through Unkey: We generate a cryptographically random key (e.g., `sk_live_abc123xyz...`) The plaintext key is returned in the API response. **This is the only time you'll see it.** We compute a SHA-256 hash of the key and store only the hash in our database. When a key is verified, we hash the provided key and compare it to the stored hash. Match = valid. ``` Your key: sk_live_abc123xyz789... Stored: a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1 ↑ SHA-256 hash (cannot be reversed) ``` This is the same approach used by GitHub, Stripe, and other security-conscious platforms. Even with full database access, an attacker cannot recover your original keys. ## Root keys Root keys authenticate your requests to the Unkey API itself. They're how you create, manage, and verify API keys programmatically. **Best practices:** * Use separate root keys for different environments (dev, staging, production) * Grant minimal permissions — only what each service needs * Rotate root keys periodically * Never commit root keys to version control [Learn more about root keys →](/security/root-keys) ## Infrastructure security | Layer | Protection | | ------------------ | -------------------------------------------------------------------------------------------- | | **Transport** | All API traffic uses TLS 1.3. No plaintext connections accepted. | | **Infrastructure** | Requests are processed across our globally distributed infrastructure, with DDoS protection. | | **Database** | Encrypted at rest and in transit. Regular backups with point-in-time recovery. | | **Access** | Internal access requires multi-factor authentication and is logged. | ## Compliance & certifications We're actively working toward SOC 2 Type II certification. If you have specific compliance requirements, [contact us](mailto:support@unkey.com) to discuss. ## Responsible disclosure Found a security issue? We appreciate responsible disclosure. * Email: [security@unkey.dev](mailto:security@unkey.dev) * Please include steps to reproduce * We'll acknowledge within 48 hours ## More security features Fine-grained access control for your root keys Restrict key verification to specific IP ranges What to do if a key is compromised Automatic detection of leaked keys in public repos Prevent accidental deletion of critical keys # Recovering Keys Source: https://unkey.com/docs/security/recovering-keys Show keys again after they are created Best practice is to create a key, show it to your users and never store it yourself. If the user loses the key, they can create a new one. This way you don't have to worry about storing the key securely. Without recovery, we would generate a new key and only store a hash of it. This way we can check if the key is correct but nobody, not even someone with access to the database, can recover the key. However there are some reasons why you might want to recover keys and show them again. * API playgrounds that need the key to call an API * Better DX for your users, it's annoying to create a new key and update it everywhere ## Vault Vault is our secure storage for secrets, such as keys. It follows a few principles: * Secrets are encrypted at rest * A leak of vaults data does not expose secrets * A leak of the main database does not expose secrets * A leak of the main encryption keys does not expose secrets An attacker would need access to the vault, the main database and the main encryption keys to decrypt the secrets. In order to make this even harder, we rotate the encryption keys regularly and do not run vault on the same servers as the main database to prevent an attacker from getting access to all the required information at once. To learn more about how it works under the hood, you can head over to our [engineering docs](https://engineering.unkey.dev/services/vault). ## Opting in By default we only store key hashes, not encrypted keys. If you want us to store keys in a way that we can recover them, you need to opt in: When creating new keys, your root key must have permission to encrypt. Head over to the [dashboard](https://app.unkey.com/settings/root-keys) and make sure the `encrypt_key` permission is enabled. Do not skip this step. Otherwise your root key will get rejected when trying to create new keys. To opt in to recovery, send us an email at [support@unkey.com](mailto:support@unkey.com?subject=Recovery%20Opt%20In). Send us the email from the email address associated with your workspace and include the `API ID` that you want to enable recovery for. Please note that this is not retroactively applied. Existing keys were never stored and cannot be recovered. Only keys created after opting in to recovery can be recovered. ## Creating keys When creating a key, you can set the `recoverable` field to `true`. This will store the key in a way that we can recover it later. ```shell theme={null} curl --request POST \ --url https://api.unkey.com/v2/keys.createKey \ --header 'Authorization: Bearer {ROOT_KEY}' \ --header 'Content-Type: application/json' \ -d '{ "apiId": "{API_ID}", "recoverable": true }' ``` ## Recovering plaintext keys Both the [getKey](/api-reference/v2/keys/get-api-key) and [listKeys](/api-reference/v2/apis/list-api-keys) endpoints accept a `decrypt` query parameter. If you set this to `true`, the key will be decrypted and returned in the response as `plaintext`. When recovering keys, your root key must have permission to decrypt. Head over to the [dashboard](https://app.unkey.com/settings/root-keys) and make sure the `decrypt_key` permission is enabled. ```shell theme={null} curl --request POST \ --url https://api.unkey.com/v2/keys.getKey \ --header 'Authorization: Bearer {ROOT_KEY}' \ --header 'Content-Type: application/json' \ -d '{ "keyId": "{KEY_ID}", "decrypt": true }' ``` ```json theme={null} { // ... "plaintext": "your-key-here" } ``` If you have any questions about recovery, please reach out to us at [support@unkey.com](mailto:support@unkey.com). For security concerns, please disclose them responsibly by emailing [security@unkey.com](mailto:security@unkey.com) instead. # Root Keys Source: https://unkey.com/docs/security/root-keys Manage your Unkey workspace with root keys Root keys authenticate *your* requests to the Unkey API. They're used to create API keys, manage identities, configure rate limits, and perform other administrative operations. Root keys have powerful permissions. Never expose them in client-side code, commit them to git, or share them publicly. ## Root keys vs API keys | | Root Keys | API Keys | | ---------------- | -------------------------------------- | ------------------------- | | **Purpose** | Manage Unkey resources | Authenticate your users | | **Who uses it** | You (the developer) | Your customers | | **Permissions** | Create/update/delete keys, manage APIs | Access your API endpoints | | **Where stored** | Your server's environment variables | Given to customers | ## Create a root key Navigate to [Settings → Root Keys](https://app.unkey.com/settings/root-keys) in your dashboard. Give the key a descriptive name and select only the permissions it needs. **Common permission sets:** | Use case | Permissions needed | | --------------------- | ---------------------------------------------------------------------------- | | Verify keys only | `api.*.read_key` | | Create keys for users | `api.*.create_key`, `api.*.read_key` | | Full key management | `api.*.create_key`, `api.*.read_key`, `api.*.update_key`, `api.*.delete_key` | | Rate limit overrides | `ratelimit.*.set_override` | Copy the key immediately — you won't see it again. Unkey only stores a hash. Store it in your environment variables: ```bash .env theme={null} UNKEY_ROOT_KEY=unkey_... ``` ## Best practices Only grant the permissions each root key actually needs. A key that only verifies API keys doesn't need `delete_key` permission. Create dedicated root keys for each service or environment: * `production-api-server` — verify and create keys * `admin-dashboard` — full management access * `billing-service` — update key credits only Even without a breach, rotate root keys every few months as a security practice. Create a new key, update your services, then delete the old one. Ensure your logging doesn't capture root keys in request bodies or headers. ## If a root key is leaked Act immediately: 1. **Revoke the key** — Go to [Settings → Root Keys](https://app.unkey.com/settings/root-keys) and delete the compromised key 2. **Create a new key** — Generate a replacement with the same permissions 3. **Update your services** — Deploy the new key to your environment 4. **Check audit logs** — Review [Audit Logs](https://app.unkey.com/audit) for any unauthorized activity 5. **Rotate affected API keys** — If you suspect API keys were created or modified, consider [rerolling](/apis/features/rerolling-key) them Enable [GitHub secret scanning](/security/github-scanning) to get automatic alerts if your root key is accidentally committed. ## Next steps Get alerts for leaked keys Monitor root key usage