diff --git a/apps/docs/api-reference/v1/migration/apis.mdx b/apps/docs/api-reference/v1/migration/apis.mdx new file mode 100644 index 0000000000..43dc164c88 --- /dev/null +++ b/apps/docs/api-reference/v1/migration/apis.mdx @@ -0,0 +1,346 @@ +--- +title: "/v1/apis.*" +description: "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" +{ + "name": "Production API" +} +``` + + +```json title="Create API Response Diff" icon="database" expandable +// 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" +# 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" +# 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 +// 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" +# 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 +// 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 +// 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" +// 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" +# 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" +{ + "apiId": "api_123" +} +``` + + +```json title="Delete API Response Diff" icon="check-circle" +// 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" +# 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" +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" +# 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" +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" +// 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 }) + }); + } +} +``` + + diff --git a/apps/docs/api-reference/v1/migration/errors.mdx b/apps/docs/api-reference/v1/migration/errors.mdx new file mode 100644 index 0000000000..59f945b82a --- /dev/null +++ b/apps/docs/api-reference/v1/migration/errors.mdx @@ -0,0 +1,276 @@ +--- +title: "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" +// 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 +// 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" +// 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" +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 + // ❌ 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 + // 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 + 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 + 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 diff --git a/apps/docs/api-reference/v1/migration/identities.mdx b/apps/docs/api-reference/v1/migration/identities.mdx new file mode 100644 index 0000000000..88f7bb8e8d --- /dev/null +++ b/apps/docs/api-reference/v1/migration/identities.mdx @@ -0,0 +1,458 @@ +--- +title: "/v1/identities.*" +description: "Migrate from key-based identity patterns to dedicated identity management in v2" +--- + +This guide covers migrating from v1's key-based identity patterns to v2's dedicated identity management system. + +## Overview + +### What's New in v2: +- **Dedicated identity endpoints**: Create, read, update, delete identities independently +- **Shared rate limits**: Identity-level rate limiting across multiple keys +- **Centralized metadata**: Single source of truth for user/entity data +- **Direct management**: No longer need to infer identities from keys + +### Migration Impact: +- **v1 Pattern**: Identity implied through key `externalId` field +- **v2 Enhancement**: Explicit identity management with dedicated endpoints +- **Benefit**: Better organization, shared resources, and centralized control + +--- + +## Creating Identities: v1 Key Pattern → v2 Dedicated Endpoint + +**v1 Approach:** Identities were implicit - you created keys with `externalId` and metadata directly on each key. + +**v2 Approach:** Create explicit identities first, then reference them from keys for shared resources. + +**Key Benefits:** +- Centralized identity metadata management +- Shared rate limits across multiple keys +- Direct identity operations without key dependency + + + +```json title="v1 vs v2: Identity Creation Patterns" expandable +// v1: Create key with identity data embedded +{ + "apiId": "api_123", // [!code --] + "externalId": "user_123", + "meta": { + "email": "user@example.com", + "plan": "pro" + }, + "ratelimit": { // [!code --] + "type": "fast", // [!code --] + "limit": 1000, // [!code --] + "refillRate": 1000, // [!code --] + "refillInterval": 3600000 // [!code --] + } // [!code --] +} + +// v2: Create identity first with shared resources +{ + "externalId": "user_123", + "meta": { + "email": "user@example.com", + "plan": "pro" + }, + "ratelimits": [ // [!code ++] + { // [!code ++] + "name": "requests", // [!code ++] + "limit": 1000, // [!code ++] + "duration": 3600000 // [!code ++] + } // [!code ++] + ] // [!code ++] +} +``` + + +```json title="Response Format Differences" icon="check-circle" expandable +// v1: Key creation response (identity data embedded) +{ + "keyId": "key_abc123def456", // [!code --] + "key": "uk_live_abc123...", // [!code --] + "apiId": "api_123", // [!code --] + "externalId": "user_123", // [!code --] + "meta": { // [!code --] + "email": "user@example.com", // [!code --] + "plan": "pro" // [!code --] + }, // [!code --] + "createdAt": "2024-01-15T10:30:00Z" // [!code --] +} + +// v2: Dedicated identity response +{ + "meta": { // [!code ++] + "requestId": "req_createidentity123" // [!code ++] + }, // [!code ++] + "data": { // [!code ++] + "identityId": "identity_abc123def456", // [!code ++] + "externalId": "user_123", // [!code ++] + "meta": { // [!code ++] + "email": "user@example.com", // [!code ++] + "plan": "pro" // [!code ++] + }, // [!code ++] + "ratelimits": [ // [!code ++] + { // [!code ++] + "id": "rl_identity_456", // [!code ++] + "name": "requests", // [!code ++] + "limit": 1000, // [!code ++] + "duration": 3600000 // [!code ++] + } // [!code ++] + ], // [!code ++] + "createdAt": "2024-01-15T10:30:00Z" // [!code ++] + } // [!code ++] +} +``` + + +```bash title="Migration Example" icon="arrow-right" +# v1: Create key with identity data +curl -X POST https://api.unkey.dev/v1/keys.createKey # [!code --] + -H "Authorization: Bearer " # [!code --] + -H "Content-Type: application/json" + -d '{"apiId": "api_123", "externalId": "user_123", "meta": {"email": "user@example.com", "plan": "pro"}}' # [!code --] + +# v2: Create identity first, then create key +curl -X POST https://api.unkey.com/v2/identities.createIdentity # [!code ++] + -H "Authorization: Bearer " # [!code ++] + -H "Content-Type: application/json" + -d '{"externalId": "user_123", "meta": {"email": "user@example.com", "plan": "pro"}, "ratelimits": [{"name": "requests", "limit": 1000, "duration": 3600000}]}' # [!code ++] + +# Then create key referencing the identity # [!code ++] +curl -X POST https://api.unkey.com/v2/keys.createKey # [!code ++] + -H "Authorization: Bearer " # [!code ++] + -H "Content-Type: application/json" + -d '{"apiId": "api_123", "externalId": "user_123"}' # [!code ++] +``` + + + +--- + +## Retrieving Identity Data: v1 Key Lookup → v2 Direct Access + +**v1 Approach:** To get identity data, you had to list/get keys and extract identity information from individual keys. + +**v2 Approach:** Directly retrieve identity data using the dedicated `getIdentity` endpoint. + + + +```bash title="v1 vs v2: Identity Retrieval Patterns" +# v1: Had to find keys by externalId to get identity data +curl -X GET "https://api.unkey.dev/v1/apis.listKeys?apiId=api_123" # [!code --] + -H "Authorization: Bearer " # [!code --] + +# Then filter results to find keys with matching externalId # [!code --] +# Identity data scattered across multiple key records # [!code --] + +# v2: Direct identity lookup +curl -X POST https://api.unkey.com/v2/identities.getIdentity # [!code ++] + -H "Authorization: Bearer " # [!code ++] + -H "Content-Type: application/json" # [!code ++] + -d '{"externalId": "user_123"}' # [!code ++] +``` + + +```json title="Identity Data Access" icon="database" expandable +// v1: Identity data from key response (partial, scattered) +{ + "keys": [ // [!code --] + { // [!code --] + "id": "key_123", // [!code --] + "externalId": "user_123", // [!code --] + "meta": {"email": "user@example.com"}, // [!code --] + "name": "Mobile App Key" // [!code --] + }, // [!code --] + { // [!code --] + "id": "key_456", // [!code --] + "externalId": "user_123", // [!code --] + "meta": {"plan": "pro"}, // [!code --] + "name": "Web App Key" // [!code --] + } // [!code --] + ] // [!code --] +} + +// v2: Complete identity data in single response +{ + "meta": { // [!code ++] + "requestId": "req_getidentity456" // [!code ++] + }, // [!code ++] + "data": { // [!code ++] + "identityId": "identity_abc123def456", // [!code ++] + "externalId": "user_123", // [!code ++] + "meta": { // [!code ++] + "email": "user@example.com", // [!code ++] + "plan": "pro" // [!code ++] + }, // [!code ++] + "ratelimits": [ // [!code ++] + { // [!code ++] + "id": "rl_identity_456", // [!code ++] + "name": "requests", // [!code ++] + "limit": 1000, // [!code ++] + "remaining": 847, // [!code ++] + "duration": 3600000, // [!code ++] + "reset": 1672534800000 // [!code ++] + } // [!code ++] + ], // [!code ++] + "createdAt": "2024-01-15T10:30:00Z" // [!code ++] + } // [!code ++] +} +``` + + + +--- + +## NEW: POST /v2/identities.updateIdentity + +**Purpose:** Update identity metadata and rate limits - not possible in v1. + +**v1 Limitation:** To update identity data, you had to update each key individually. + +**v2 Enhancement:** Update identity once, changes apply to all associated keys. + + + +```bash title="v1 vs v2: Identity Update Patterns" +# v1: Had to update each key separately - very inefficient +curl -X POST https://api.unkey.dev/v1/keys.updateKey # [!code --] + -H "Authorization: Bearer " # [!code --] + -H "Content-Type: application/json" + -d '{"keyId": "key_123", "meta": {"plan": "enterprise"}}' # [!code --] + +curl -X POST https://api.unkey.dev/v1/keys.updateKey # [!code --] + -H "Authorization: Bearer " # [!code --] + -H "Content-Type: application/json" + -d '{"keyId": "key_456", "meta": {"plan": "enterprise"}}' # [!code --] +# ... repeat for every key with this externalId # [!code --] + +# v2: Update identity once, affects all associated keys +curl -X POST https://api.unkey.com/v2/identities.updateIdentity # [!code ++] + -H "Authorization: Bearer " # [!code ++] + -H "Content-Type: application/json" # [!code ++] + -d '{"externalId": "user_123", "meta": {"plan": "enterprise"}, "ratelimits": [{"name": "requests", "limit": 5000, "duration": 3600000}]}' # [!code ++] +``` + + +```json title="Update Identity Response" icon="check-circle" expandable +{ + "meta": { + "requestId": "req_updateidentity789" + }, + "data": { + "identityId": "identity_abc123def456", + "externalId": "user_123", + "meta": { + "email": "user@example.com", + "plan": "enterprise" + }, + "ratelimits": [ + { + "id": "rl_identity_456", + "name": "requests", + "limit": 5000, + "duration": 3600000 + } + ], + "updatedAt": "2024-01-15T11:45:00Z" + } +} +``` + + + +--- + +## NEW: POST /v2/identities.listIdentities + +**Purpose:** Get paginated list of all identities - not possible in v1. + +**v1 Limitation:** No direct way to list identities; had to infer from key listings. + +**v2 Enhancement:** Direct identity listing with metadata and pagination. + + + +```bash title="v1 vs v2: Identity Listing Patterns" +# v1: List all keys and manually group by externalId +curl -X GET "https://api.unkey.dev/v1/apis.listKeys?apiId=api_123" # [!code --] + -H "Authorization: Bearer " # [!code --] + +# Then manually process results to extract unique externalIds # [!code --] +# No direct identity metadata or pagination # [!code --] + +# v2: Direct identity listing with pagination +curl -X POST https://api.unkey.com/v2/identities.listIdentities # [!code ++] + -H "Authorization: Bearer " # [!code ++] + -H "Content-Type: application/json" # [!code ++] + -d '{"limit": 100, "cursor": "optional_cursor"}' # [!code ++] +``` + + +```json title="List Identities Response" icon="list" expandable +{ + "meta": { + "requestId": "req_listidentities789" + }, + "data": { + "identities": [ + { + "identityId": "identity_abc123", + "externalId": "user_123", + "meta": { + "email": "user@example.com", + "plan": "pro" + }, + "createdAt": "2024-01-15T10:30:00Z" + }, + { + "identityId": "identity_def456", + "externalId": "user_456", + "meta": { + "email": "user2@example.com", + "plan": "enterprise" + }, + "createdAt": "2024-01-16T09:15:00Z" + } + ], + "cursor": "next_page_cursor_here" + } +} +``` + + + +--- + +## NEW: POST /v2/identities.deleteIdentity + +**Purpose:** Permanently delete an identity - not directly possible in v1. + +**v1 Limitation:** To remove identity, had to delete all associated keys individually. + +**v2 Enhancement:** Delete identity while preserving or handling associated keys appropriately. + + + +```bash title="v1 vs v2: Identity Deletion Patterns" +# v1: Had to delete each key individually to remove identity +curl -X POST https://api.unkey.dev/v1/keys.deleteKey # [!code --] + -H "Authorization: Bearer " # [!code --] + -H "Content-Type: application/json" + -d '{"keyId": "key_123"}' # [!code --] + +curl -X POST https://api.unkey.dev/v1/keys.deleteKey # [!code --] + -H "Authorization: Bearer " # [!code --] + -H "Content-Type: application/json" + -d '{"keyId": "key_456"}' # [!code --] +# ... repeat for every key with this externalId # [!code --] + +# v2: Delete identity (associated keys remain but lose shared resources) +curl -X POST https://api.unkey.com/v2/identities.deleteIdentity # [!code ++] + -H "Authorization: Bearer " # [!code ++] + -H "Content-Type: application/json" # [!code ++] + -d '{"externalId": "user_123"}' # [!code ++] +``` + + +```json title="Delete Identity Response" icon="check-circle" +{ + "meta": { + "requestId": "req_deleteidentity999" + }, + "data": {} +} +``` + + + +--- + +## Migration Patterns + +### Response Format Migration + + + +```typescript title="v1 vs v2: Identity Data Access Patterns" +// v1: Get identity data by listing keys +const response = await fetch('/v1/apis.listKeys?apiId=api_123', { // [!code --] + method: 'GET', // [!code --] + headers: { // [!code --] + 'Authorization': 'Bearer ' // [!code --] + } // [!code --] +}); // [!code --] + +const data = await response.json(); // [!code --] +// Filter keys by externalId and aggregate identity data // [!code --] +const userKeys = data.keys.filter(key => key.externalId === 'user_123'); // [!code --] +const identityMeta = {}; // Manually merge metadata from all keys // [!code --] + +// v2: Direct identity retrieval +const response = await fetch('/v2/identities.getIdentity', { // [!code ++] + method: 'POST', // [!code ++] + headers: { // [!code ++] + 'Authorization': 'Bearer ', // [!code ++] + 'Content-Type': 'application/json' // [!code ++] + }, // [!code ++] + body: JSON.stringify({ externalId: 'user_123' }) // [!code ++] +}); // [!code ++] + +const result = await response.json(); // [!code ++] +const identityData = result.data; // Complete identity data // [!code ++] +const requestId = result.meta.requestId; // For debugging // [!code ++] +``` + + + +### Identity-Key Relationship Patterns + + + +```typescript title="v1 vs v2: Identity-Key Relationship Patterns" +// v1: Create keys with identity data embedded +const keyResponse = await fetch('/v1/keys.createKey', { // [!code --] + method: 'POST', + headers: { + 'Authorization': 'Bearer ', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + apiId: 'api_123', + externalId: 'user_123', // [!code --] + meta: { email: 'user@example.com', plan: 'pro' } // [!code --] + }) +}); + +// v2: Create identity first +const identityResponse = await fetch('/v2/identities.createIdentity', { // [!code ++] + method: 'POST', // [!code ++] + headers: { // [!code ++] + 'Authorization': 'Bearer ', // [!code ++] + 'Content-Type': 'application/json' // [!code ++] + }, // [!code ++] + body: JSON.stringify({ // [!code ++] + externalId: 'user_123', // [!code ++] + meta: { email: 'user@example.com', plan: 'pro' }, // [!code ++] + ratelimits: [{ name: 'requests', limit: 1000, duration: 3600000 }] // [!code ++] + }) // [!code ++] +}); // [!code ++] + +// Then create keys that reference the identity // [!code ++] +const keyResponse = await fetch('/v2/keys.createKey', { // [!code ++] + method: 'POST', // [!code ++] + headers: { // [!code ++] + 'Authorization': 'Bearer ', // [!code ++] + 'Content-Type': 'application/json' // [!code ++] + }, // [!code ++] + body: JSON.stringify({ // [!code ++] + apiId: 'api_123', // [!code ++] + externalId: 'user_123' // References the identity // [!code ++] + }) // [!code ++] +}); // [!code ++] +``` + + diff --git a/apps/docs/api-reference/v1/migration/index.mdx b/apps/docs/api-reference/v1/migration/index.mdx new file mode 100644 index 0000000000..8580ea9674 --- /dev/null +++ b/apps/docs/api-reference/v1/migration/index.mdx @@ -0,0 +1,122 @@ +--- +title: "Overview" +description: "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 +npm install @unkey/api@latest +``` + + +```bash +pip install unkey-py@^2.0.0 +``` + + +```bash +go get github.com/unkeyed/unkey-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 +{ + "valid": true, + "keyId": "key_123", + "name": "Production API Key" +} +``` + +#### v2 Response Format +```json +{ + "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 + + +Error response format changes and codes + + + + +### Getting Help + +- **Documentation:** https://unkey.com/docs/api-reference/v2 +- **Discord:** https://unkey.com/discord +- **GitHub Issues:** https://github.com/unkeyed/unkey/issues +- **Email Support:** support@unkey.dev +- **Book a Call:** https://cal.com/team/unkey/founders - Schedule time with our team for migration help or feedback diff --git a/apps/docs/api-reference/v1/migration/keys.mdx b/apps/docs/api-reference/v1/migration/keys.mdx new file mode 100644 index 0000000000..dd5c8377b8 --- /dev/null +++ b/apps/docs/api-reference/v1/migration/keys.mdx @@ -0,0 +1,1318 @@ +--- +title: "/v1/keys.*" +description: "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 title="Key Creation Request Diff" icon="key" expandable +{ + "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 title="Create Key Response Diff" icon="check-circle" expandable +// 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 title="v1 cURL" icon="terminal" expandable +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 title="v2 cURL" icon="terminal" expandable highlight={4,12-18,19-26} +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:** +- 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 +- **Important**: v1 doesn't require root key authentication, v2 does + +**Simple Key Verification** + + + +```json title="Key Verification Request Changes" icon="code" +{ + "key": "sk_1234abcdef" +} +``` + +```bash title="v1 cURL" icon="terminal" +curl -X POST https://api.unkey.dev/v1/keys.verifyKey \ + -H "Content-Type: application/json" \ + -d '{"key": "sk_1234abcdef"}' +``` + +```bash title="v2 cURL" icon="terminal" highlight={2} +curl -X POST https://api.unkey.com/v2/keys.verifyKey \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"key": "sk_1234abcdef"}' +``` + + +```json title="Response Format Changes" icon="arrow-right" expandable +// 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 ++] + "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 title="Permission Query Syntax" icon="shield" +// 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 title="v1 cURL" icon="terminal" +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 title="v2 cURL" icon="terminal" highlight={2,6} +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 title="Credits & Rate Limits Structure" icon="coins" +// 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 title="v1 cURL" icon="terminal" +curl -X POST https://api.unkey.dev/v1/keys.verifyKey \ + -H "Content-Type: application/json" \ + -d '{ + "key": "sk_1234abcdef", + "remaining": {"cost": 5} + }' +``` + +```bash title="v2 cURL" icon="terminal" highlight={2,6-10} +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 + }] + }' +``` + + + +--- + +## 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 title="HTTP Method Change" icon="arrow-right" +# v1: GET with query parameters +curl -X GET "https://api.unkey.dev/v1/keys.getKey?keyId=key_123" \ # [!code --] + -H "Authorization: Bearer " # [!code --] + +# v2: POST with request body +curl -X POST https://api.unkey.com/v2/keys.getKey \ # [!code ++] + -H "Authorization: Bearer " \ # [!code ++] + -H "Content-Type: application/json" \ # [!code ++] + -d '{"keyId": "key_123"}' # [!code ++] +``` + + +```json title="Get Key Response Diff" icon="database" expandable +// 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 ++] + "apiId": "api_abc", // [!code ++] + "name": "Production API Key", // [!code ++] + "prefix": "prod", // [!code ++] + "start": "prod_1234", // [!code ++] + "externalId": "customer_789", // [!code ++] + "meta": { // [!code ++] + "plan": "enterprise" // [!code ++] + }, // [!code ++] + "createdAt": "2024-01-15T10:30:00Z", // [!code ++] + "expires": null, // [!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 ++] + "enabled": true, // [!code ++] + "permissions": ["documents.read"], // [!code ++] + "roles": ["editor"] // [!code ++] + } // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +curl -X GET "https://api.unkey.dev/v1/keys.getKey?keyId=key_123" \ + -H "Authorization: Bearer " +``` + +```bash title="v2 cURL" icon="terminal" highlight={1,3} +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 title="Delete Key Request" icon="trash" expandable +// v1 Request +{ + "keyId": "key_123" +} + +// v2 Request (enhanced) +{ + "keyId": "key_123", + "permanent": false // [!code ++] +} +``` + + +```json title="Delete Key Response Diff" icon="check-circle" +// v1 Response (direct empty response) +{} // [!code --] + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_deletekey789" // [!code ++] + }, // [!code ++] + "data": {} // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +curl -X POST https://api.unkey.dev/v1/keys.deleteKey \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "keyId": "key_123" + }' +``` + +```bash title="v2 cURL" icon="terminal" highlight={1,6} +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 title="Update Key Request Diff" icon="edit" expandable +// 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 title="Update Key Response Diff" icon="check-circle" +// v1 Response (direct empty response) +{} // [!code --] + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_updatekey456" // [!code ++] + }, // [!code ++] + "data": { // [!code ++] + "keyId": "key_123", // [!code ++] + "updatedAt": "2024-01-15T11:30:00Z" // [!code ++] + } // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +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 title="v2 cURL" icon="terminal" highlight={1,6-8} +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` +- New operation types: `set`, `increment`, `decrement` +- Response format: Direct response → `{meta, data}` envelope + + + +```json title="Update Credits Request Diff" icon="coins" expandable +// v1 Request +{ + "keyId": "key_123", + "value": 1000 // [!code --] +} + +// v2 Request +{ + "keyId": "key_123", + "operation": "set", // [!code ++] + "value": 1000 // [!code focus] +} +``` + + +```json title="Update Credits Response Diff" icon="check-circle" expandable +// v1 Response (direct response) +{ + "remaining": 1000 // [!code --] +} + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_updatecredits123" // [!code ++] + }, // [!code ++] + "data": { // [!code ++] + "keyId": "key_123", // [!code ++] + "credits": { // [!code ++] + "remaining": 1000 // [!code ++] + } // [!code ++] + } // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +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 title="v2 cURL" icon="terminal" highlight={1,6} +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 +- Added `meta.requestId` for debugging + + + +```json title="Whoami Request" icon="user" +// v1 & v2 Request (unchanged) +{} +``` + + +```json title="Whoami Response Diff" icon="database" expandable +// 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 ++] + "apiId": "api_abc123", // [!code ++] + "workspaceId": "ws_xyz789", // [!code ++] + "permissions": ["documents.read"], // [!code ++] + "roles": ["editor"], // [!code ++] + "createdAt": "2024-01-15T10:30:00Z" // [!code ++] + } // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +curl -X POST https://api.unkey.dev/v1/keys.whoami \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +```bash title="v2 cURL" icon="terminal" highlight={1} +curl -X POST https://api.unkey.com/v2/keys.whoami \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + + + +--- + +## 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 +- Auto-creation of permissions if they don't exist +- Added `meta.requestId` for debugging + + + +```json title="Add Permissions Request" icon="shield-plus" expandable +// v1 & v2 Request (unchanged) +{ + "keyId": "key_123", + "permissions": [ + { + "name": "documents.read", + "description": "Read access to documents" + }, + { + "name": "documents.write", + "description": "Write access to documents" + } + ] +} +``` + + +```json title="Add Permissions Response Diff" icon="check-circle" +// v1 Response (array of created permissions) +[ + { + "id": "perm_123", // [!code --] + "name": "documents.read" // [!code --] + }, + { + "id": "perm_456", // [!code --] + "name": "documents.write" // [!code --] + } +] // [!code --] + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_addperms123" // [!code ++] + }, // [!code ++] + "data": {} // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +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 title="v2 cURL" icon="terminal" highlight={1} +curl -X POST https://api.unkey.com/v2/keys.addPermissions \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "keyId": "key_123", + "permissions": [ + { + "name": "documents.read", + "description": "Read access to documents" + } + ] + }' +``` + + + +--- + +### 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 +- Added `meta.requestId` for debugging + + + +```json title="Remove Permissions Request" icon="shield-minus" +// v1 & v2 Request (unchanged) +{ + "keyId": "key_123", + "permissions": ["documents.write", "documents.delete"] +} +``` + + +```json title="Remove Permissions Response Diff" icon="check-circle" +// v1 Response (direct empty response) +{} // [!code --] + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_removeperms456" // [!code ++] + }, // [!code ++] + "data": {} // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +curl -X POST https://api.unkey.dev/v1/keys.removePermissions \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "keyId": "key_123", + "permissions": ["documents.write", "documents.delete"] + }' +``` + +```bash title="v2 cURL" icon="terminal" highlight={1} +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 +- Added `meta.requestId` for debugging + + + +```json title="Set Permissions Request" icon="shield-check" expandable +// v1 & v2 Request (unchanged) +{ + "keyId": "key_123", + "permissions": [ + { + "name": "documents.read", + "description": "Read access to documents" + }, + { + "name": "comments.moderate", + "description": "Moderate comments" + } + ] +} +``` + + +```json title="Set Permissions Response Diff" icon="check-circle" +// 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 ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +curl -X POST https://api.unkey.dev/v1/keys.setPermissions \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "keyId": "key_123", + "permissions": [ + { + "name": "documents.read", + "description": "Read access to documents" + } + ] + }' +``` + +```bash title="v2 cURL" icon="terminal" highlight={1} +curl -X POST https://api.unkey.com/v2/keys.setPermissions \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "keyId": "key_123", + "permissions": [ + { + "name": "documents.read", + "description": "Read access to documents" + } + ] + }' +``` + + + +--- + +## 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 +- Auto-creation of roles if they don't exist +- Added `meta.requestId` for debugging + + + +```json title="Add Roles Request" icon="user-plus" +// v1 & v2 Request (unchanged) +{ + "keyId": "key_123", + "roles": ["editor", "moderator"] +} +``` + + +```json title="Add Roles Response Diff" icon="check-circle" +// v1 Response (array of added roles) +[ + { + "id": "role_123", // [!code --] + "name": "editor" // [!code --] + }, + { + "id": "role_456", // [!code --] + "name": "moderator" // [!code --] + } +] // [!code --] + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_addroles123" // [!code ++] + }, // [!code ++] + "data": {} // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +curl -X POST https://api.unkey.dev/v1/keys.addRoles \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "keyId": "key_123", + "roles": ["editor", "moderator"] + }' +``` + +```bash title="v2 cURL" icon="terminal" highlight={1} +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 +- Added `meta.requestId` for debugging + + + +```json title="Remove Roles Request" icon="user-minus" +// v1 & v2 Request (unchanged) +{ + "keyId": "key_123", + "roles": ["moderator"] +} +``` + + +```json title="Remove Roles Response Diff" icon="check-circle" +// v1 Response (direct empty response) +{} // [!code --] + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_removeroles456" // [!code ++] + }, // [!code ++] + "data": {} // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +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 title="v2 cURL" icon="terminal" highlight={1} +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 +- Added `meta.requestId` for debugging + + + +```json title="Set Roles Request" icon="users-cog" +// v1 & v2 Request (unchanged) +{ + "keyId": "key_123", + "roles": ["editor", "admin"] +} +``` + + +```json title="Set Roles Response Diff" icon="check-circle" +// v1 Response (array of all roles on key) +[ + { + "id": "role_123", // [!code --] + "name": "editor" // [!code --] + }, + { + "id": "role_789", // [!code --] + "name": "admin" // [!code --] + } +] // [!code --] + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_setroles789" // [!code ++] + }, // [!code ++] + "data": {} // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +curl -X POST https://api.unkey.dev/v1/keys.setRoles \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "keyId": "key_123", + "roles": ["editor", "admin"] + }' +``` + +```bash title="v2 cURL" icon="terminal" highlight={1} +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 title="v1 vs v2: Response Handling" +// 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 title="v1 vs v2: Key Structure" +// 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 +{ + "apiId": "api_123", + "externalId": "user_456", // [!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 +- [ ] Add root key authentication header to key verification calls +- [ ] Convert permission query objects to strings: `"perm1 AND perm2"` +- [ ] Update `remaining` → `credits` for cost parameters +- [ ] Handle new rate limits array structure in responses + +### 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 diff --git a/apps/docs/api-reference/v1/migration/latency_drop.png b/apps/docs/api-reference/v1/migration/latency_drop.png new file mode 100644 index 0000000000..63718477d0 Binary files /dev/null and b/apps/docs/api-reference/v1/migration/latency_drop.png differ diff --git a/apps/docs/api-reference/v1/migration/permissions.mdx b/apps/docs/api-reference/v1/migration/permissions.mdx new file mode 100644 index 0000000000..7f2514759b --- /dev/null +++ b/apps/docs/api-reference/v1/migration/permissions.mdx @@ -0,0 +1,666 @@ +--- +title: "/v1/permissions.*" +description: "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" +# 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 '{"name": "documents.read", "description": "Read access to documents"}' # [!code ++] +``` + + +```json title="Create Permission Response" icon="check-circle" expandable +{ + "meta": { + "requestId": "req_createpermission123" + }, + "data": { + "permissionId": "perm_abc123def456", + "name": "documents.read", + "description": "Read access to documents", + "createdAt": "2024-01-15T10:30:00Z" + } +} +``` + + + +--- + +## 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 + + + +```bash title="v1 vs v2: Direct Permission Retrieval" icon="shield" +# 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 '{"name": "documents.read"}' # [!code ++] +``` + + +```json title="Get Permission Response" icon="database" expandable +{ + "meta": { + "requestId": "req_getpermission456" + }, + "data": { + "permissionId": "perm_abc123def456", + "name": "documents.read", + "description": "Read access to documents", + "createdAt": "2024-01-15T10:30:00Z" + } +} +``` + + + +--- + +## 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" +# 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 +{ + "meta": { + "requestId": "req_listpermissions789" + }, + "data": [ + { + "permissionId": "perm_abc123", + "name": "documents.read", + "description": "Read access to documents", + "createdAt": "2024-01-15T10:30:00Z" + }, + { + "permissionId": "perm_def456", + "name": "documents.write", + "description": "Write access to documents", + "createdAt": "2024-01-16T09:15:00Z" + } + ], + "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 +- Domain change: `api.unkey.dev` → `api.unkey.com` + + + +```bash title="v1 vs v2: Global Permission Deletion" icon="shield-minus" +# 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 '{"name": "documents.read"}' # [!code ++] +``` + + +```json title="Delete Permission Response" icon="check-circle" +{ + "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" +# 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 +{ + "meta": { + "requestId": "req_createrole123" + }, + "data": { + "roleId": "role_abc123def456", + "name": "editor", + "description": "Content editor role", + "permissions": [ + "documents.read", + "documents.write", + "comments.moderate" + ], + "createdAt": "2024-01-15T10:30:00Z" + } +} +``` + + +```bash title="Assign Role to Key" icon="key" +# 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 + + + +```bash title="v1 vs v2: Role Retrieval" icon="user" +# 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 '{"name": "editor"}' # [!code ++] +``` + + +```json title="Get Role Response" icon="database" expandable +{ + "meta": { + "requestId": "req_getrole456" + }, + "data": { + "roleId": "role_abc123def456", + "name": "editor", + "description": "Content editor role", + "permissions": [ + "documents.read", + "documents.write", + "comments.moderate" + ], + "createdAt": "2024-01-15T10:30:00Z" + } +} +``` + + + +### GET /v1/permissions.listRoles → POST /v2/permissions.listRoles + + + +```bash title="v1 vs v2: Role Listing" icon="user-group" +# 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 +{ + "meta": { + "requestId": "req_listroles789" + }, + "data": [ + { + "roleId": "role_abc123", + "name": "editor", + "description": "Content editor role", + "permissions": [ + "documents.read", + "documents.write", + "comments.moderate" + ], + "createdAt": "2024-01-15T10:30:00Z" + }, + { + "roleId": "role_def456", + "name": "admin", + "description": "Full administrative access", + "permissions": [ + "documents.*", + "users.*", + "settings.*" + ], + "createdAt": "2024-01-16T09:15:00Z" + } + ], + "pagination": { + "cursor": "next_page_cursor_here", + "hasMore": true + } +} +``` + + + +### POST /v1/permissions.deleteRole → POST /v2/permissions.deleteRole + + + +```bash title="v1 vs v2: Role Deletion" icon="user-minus" +# 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 '{"name": "editor"}' # [!code ++] +``` + + +```json title="Delete Role Response" icon="check-circle" +{ + "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" +// 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" +// 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" +// 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" +# 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" +// 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" +// 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" +{ + "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" +{ + "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" +{ + "keyId": "key_123", + "permissions": [ + { + "name": "new.permission", + "description": "This will be auto-created if it doesn't exist" + } + ] +} +``` +Permissions and roles are automatically created when referenced if they don't exist. + +### Simplified Query Syntax +```bash title="String-based permission queries" icon="code" +# Simple AND +"permissions": "read AND write" + +# Simple OR +"permissions": "read OR write" + +# Complex with parentheses +"permissions": "read AND (write OR delete)" + +# Wildcard support +"permissions": "documents.*" +``` + +### Enhanced Management (Both v1 and v2) +- List all permissions and roles in workspace +- Delete permissions/roles globally (affects all keys) +- Audit permission usage across keys +- Build management dashboards with dedicated endpoints +- v2 adds request IDs for better debugging + +--- + +## 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 + +### Enhanced Features +- [ ] Build centralized permission and role management systems +- [ ] Implement RBAC dashboards using list endpoints +- [ ] Use global deletion for permission/role cleanup +- [ ] Leverage wildcard permissions for hierarchical access control + +### 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 +- [ ] Validate global deletion still works correctly diff --git a/apps/docs/api-reference/v1/migration/ratelimiting.mdx b/apps/docs/api-reference/v1/migration/ratelimiting.mdx new file mode 100644 index 0000000000..5b82092f72 --- /dev/null +++ b/apps/docs/api-reference/v1/migration/ratelimiting.mdx @@ -0,0 +1,757 @@ +--- +title: "/v1/ratelimits.*" +description: "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**: Improved `autoApply` parameter (formerly `async`) + +### 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" +{ + "namespace": "email_sending", + "identifier": "user_123", + "limit": 100, + "duration": 3600000, + "cost": 1 +} +``` + + +```json title="Rate Limit Response Diff" icon="database" expandable +// 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 ++] + "identifier": "user_123", // [!code ++] + "namespace": "email_sending", // [!code ++] + "cost": 1 // [!code ++] + } // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +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" +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 +{ + "namespace": "api_requests", + "identifier": "premium_user_456", + "limit": 10000, + "duration": 3600000, + "async": false +} +``` + + +```json title="Set Override Response Diff" icon="check-circle" expandable +// v1 Response (direct response) +{ + "overrideId": "over_123" // [!code --] +} + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_setoverride456" // [!code ++] + }, // [!code ++] + "data": { // [!code ++] + "overrideId": "override_abc123", // [!code ++] + "namespace": "api_requests", // [!code ++] + "identifier": "premium_user_456", // [!code ++] + "limit": 10000, // [!code ++] + "duration": 3600000, // [!code ++] + "async": false, // [!code ++] + "createdAt": "2024-01-15T10:30:00Z" // [!code ++] + } // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +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" +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, + "async": false + }' +``` + + + +--- + +### 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" +{ + "namespace": "api_requests", + "identifier": "premium_user_456" +} +``` + +```json title="Get Override Response Diff" icon="database" expandable +// 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 ++] + "namespace": "api_requests", // [!code ++] + "identifier": "premium_user_456", // [!code ++] + "limit": 10000, // [!code ++] + "duration": 3600000, // [!code ++] + "async": false, // [!code ++] + "createdAt": "2024-01-15T10:30:00Z" // [!code ++] + } // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +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" +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" +{ + "namespace": "api_requests", + "limit": 100, + "cursor": "optional_cursor" +} +``` + + +```json title="List Overrides Response Diff" icon="database" expandable +// 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 ++] + "namespace": "api_requests", // [!code ++] + "identifier": "premium_user_456", // [!code ++] + "limit": 10000, // [!code ++] + "duration": 3600000, // [!code ++] + "async": false, // [!code ++] + "createdAt": "2024-01-15T10:30:00Z" // [!code ++] + } // [!code ++] + ], // [!code ++] + "cursor": "next_page_cursor_here" // [!code ++] + } // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +curl -X GET "https://api.unkey.dev/v1/ratelimits.listOverrides?namespaceName=api_requests&limit=100" \ + -H "Authorization: Bearer " +``` + +```bash title="v2 cURL" icon="terminal" +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" +{ + "namespace": "api_requests", + "identifier": "premium_user_456" +} +``` + +```json title="Delete Override Response Diff" icon="check-circle" +// v1 Response (direct empty response) +{} // [!code --] + +// v2 Response +{ + "meta": { // [!code ++] + "requestId": "req_deleteoverride999" // [!code ++] + }, // [!code ++] + "data": {} // [!code ++] +} +``` + + +```bash title="v1 cURL" icon="terminal" +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" +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 +// 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 +// 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 +// 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" +// 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" +// 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" +// 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" +// 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 +{ + "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" +{ + "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" +// 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 +- [ ] Change `async` parameter to `autoApply` +- [ ] 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 diff --git a/apps/docs/api-reference/v1/overview.mdx b/apps/docs/api-reference/v1/overview.mdx index be46561759..bb41feb990 100644 --- a/apps/docs/api-reference/v1/overview.mdx +++ b/apps/docs/api-reference/v1/overview.mdx @@ -3,6 +3,8 @@ title: Overview description: General information about the API. --- +The v1 API is deprecated and will be removed in January 2026. Please [migrate to the v2 API](/api-reference/v1/migration). + The Unkey API uses HTTP RPC-style methods and generally follow the schema: ``` diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 5b15f1bdbd..86835ffe72 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -224,7 +224,23 @@ "groups": [ { "group": "Introduction", - "pages": ["api-reference/v1/overview", "api-reference/v1/authentication"] + "pages": [ + "api-reference/v1/overview", + "api-reference/v1/authentication", + { + "group": "Migration Guide", + "icon": "arrows-rotate", + "pages": [ + "api-reference/v1/migration/index", + "api-reference/v1/migration/keys", + "api-reference/v1/migration/apis", + "api-reference/v1/migration/identities", + "api-reference/v1/migration/permissions", + "api-reference/v1/migration/ratelimiting", + "api-reference/v1/migration/errors" + ] + } + ] }, { "group": "Endpoints", diff --git a/go/apps/api/openapi/gen.go b/go/apps/api/openapi/gen.go index d3a8fe187d..c0448ec41f 100644 --- a/go/apps/api/openapi/gen.go +++ b/go/apps/api/openapi/gen.go @@ -414,33 +414,6 @@ type RatelimitResponse struct { Name string `json:"name"` } -// Role defines model for Role. -type Role struct { - // Description Optional detailed explanation of what this role encompasses and what access it provides. - // Helps team members understand the role's scope, intended use cases, and security implications. - // Include information about what types of users should receive this role and what they can accomplish. - // Not visible to end users - this is for internal documentation and access control audits. - Description *string `json:"description,omitempty"` - - // Id The unique identifier for this role within Unkey's system. - // Generated automatically when the role is created and used to reference this role in API operations. - // Always begins with 'role_' followed by alphanumeric characters and underscores. - Id string `json:"id"` - - // Name The human-readable name for this role that describes its function. - // Should be descriptive enough for administrators to understand what access this role provides. - // Use clear, semantic names that reflect the job function or responsibility level. - // Names must be unique within your workspace to avoid confusion during role assignment. - Name string `json:"name"` - - // Permissions Complete list of permissions currently assigned to this role. - // Each permission grants specific access rights that will be inherited by any keys or users assigned this role. - // Use this list to understand the full scope of access provided by this role. - // Permissions can be added or removed from roles without affecting the role's identity or other properties. - // Empty array indicates a role with no permissions currently assigned. - Permissions []Permission `json:"permissions"` -} - // UnauthorizedErrorResponse Error response when authentication has failed or credentials are missing. This occurs when: // - No authentication token is provided in the request // - The provided token is invalid, expired, or malformed @@ -1931,6 +1904,33 @@ type VerifyKeyRatelimitData struct { Reset int64 `json:"reset"` } +// Role defines model for role. +type Role struct { + // Description Optional detailed explanation of what this role encompasses and what access it provides. + // Helps team members understand the role's scope, intended use cases, and security implications. + // Include information about what types of users should receive this role and what they can accomplish. + // Not visible to end users - this is for internal documentation and access control audits. + Description *string `json:"description,omitempty"` + + // Id The unique identifier for this role within Unkey's system. + // Generated automatically when the role is created and used to reference this role in API operations. + // Always begins with 'role_' followed by alphanumeric characters and underscores. + Id string `json:"id"` + + // Name The human-readable name for this role that describes its function. + // Should be descriptive enough for administrators to understand what access this role provides. + // Use clear, semantic names that reflect the job function or responsibility level. + // Names must be unique within your workspace to avoid confusion during role assignment. + Name string `json:"name"` + + // Permissions Complete list of permissions currently assigned to this role. + // Each permission grants specific access rights that will be inherited by any keys or users assigned this role. + // Use this list to understand the full scope of access provided by this role. + // Permissions can be added or removed from roles without affecting the role's identity or other properties. + // Empty array indicates a role with no permissions currently assigned. + Permissions []Permission `json:"permissions"` +} + // ChproxyMetricsJSONRequestBody defines body for ChproxyMetrics for application/json ContentType. type ChproxyMetricsJSONRequestBody = ChproxyMetricsRequestBody diff --git a/go/apps/api/openapi/openapi-generated.yaml b/go/apps/api/openapi/openapi-generated.yaml index f8883e6f98..36024380c7 100644 --- a/go/apps/api/openapi/openapi-generated.yaml +++ b/go/apps/api/openapi/openapi-generated.yaml @@ -2811,7 +2811,7 @@ components: type: object properties: role: - "$ref": "#/components/schemas/Role" + "$ref": "#/components/schemas/role" required: - role additionalProperties: false @@ -2827,7 +2827,7 @@ components: maxItems: 1000 description: Array of roles with their assigned permissions. items: - "$ref": "#/components/schemas/Role" + "$ref": "#/components/schemas/role" V2RatelimitDeleteOverrideResponseData: type: object additionalProperties: false diff --git a/go/apps/api/openapi/spec/paths/v2/permissions/getRole/V2PermissionsGetRoleResponseData.yaml b/go/apps/api/openapi/spec/paths/v2/permissions/getRole/V2PermissionsGetRoleResponseData.yaml index 7d6c268a18..b67f1238f7 100644 --- a/go/apps/api/openapi/spec/paths/v2/permissions/getRole/V2PermissionsGetRoleResponseData.yaml +++ b/go/apps/api/openapi/spec/paths/v2/permissions/getRole/V2PermissionsGetRoleResponseData.yaml @@ -1,7 +1,7 @@ type: object properties: role: - "$ref": "../../../../common/Role.yaml" + "$ref": "../../../../common/role.yaml" required: - role additionalProperties: false diff --git a/go/apps/api/openapi/spec/paths/v2/permissions/listRoles/V2PermissionsListRolesResponseData.yaml b/go/apps/api/openapi/spec/paths/v2/permissions/listRoles/V2PermissionsListRolesResponseData.yaml index 31fb8044ea..e0c4bf1f5a 100644 --- a/go/apps/api/openapi/spec/paths/v2/permissions/listRoles/V2PermissionsListRolesResponseData.yaml +++ b/go/apps/api/openapi/spec/paths/v2/permissions/listRoles/V2PermissionsListRolesResponseData.yaml @@ -2,4 +2,4 @@ type: array maxItems: 1000 description: Array of roles with their assigned permissions. items: - "$ref": "../../../../common/Role.yaml" + "$ref": "../../../../common/role.yaml" diff --git a/go/benchmarks/keyverify.js b/go/benchmarks/keyverify.js index 8aa6d314aa..923c9e8127 100644 --- a/go/benchmarks/keyverify.js +++ b/go/benchmarks/keyverify.js @@ -7,24 +7,24 @@ const requestLatencyTrend = new Trend("request_latency", true); const loadZones = [ "amazon:us:ashburn", // US East - // 'amazon:us:portland', // US West + //'amazon:us:portland', // US West // 'amazon:ie:dublin', // Europe West - // 'amazon:de:frankfurt', // Europe Central + "amazon:de:frankfurt", // Europe Central // 'amazon:sg:singapore', // Asia Pacific - // 'amazon:jp:tokyo', // Asia Pacific East - // 'amazon:au:sydney', // Australia + "amazon:jp:tokyo", // Asia Pacific East + "amazon:au:sydney", // Australia // 'amazon:br:sao paulo', // South America - // 'amazon:in:mumbai', // India + "amazon:in:mumbai", // India // 'amazon:ca:montreal' // Canada ]; -const equalPercent = Math.floor(100 / loadZones.length); +const percent = Math.floor(100 / loadZones.length); + const distribution = {}; -loadZones.forEach((zone, index) => { +loadZones.forEach((zone) => { distribution[zone] = { loadZone: zone, - percent: - index === loadZones.length - 1 ? 100 - equalPercent * (loadZones.length - 1) : equalPercent, + percent: percent, }; }); @@ -33,9 +33,27 @@ export const options = { project: "3788521", distribution: distribution, }, - stages: [ - { duration: "10m", target: 10 }, // 10 req/s for 1 minute - ], + scenarios: { + api_v1_keyverify: { + executor: "constant-arrival-rate", + rate: 10, + timeUnit: "1s", + duration: "5m", + preAllocatedVUs: 10, + maxVUs: 15, + exec: "testV1KeyVerify", + }, + api_v2_keyverify: { + executor: "constant-arrival-rate", + rate: 10, + timeUnit: "1s", + duration: "5m", + startTime: "5m", + preAllocatedVUs: 10, + maxVUs: 15, + exec: "testV2KeyVerify", + }, + }, thresholds: { http_req_duration: ["p(95)<500"], // 95% of requests must complete below 500ms checks: ["rate>0.99"], // 99% of checks must pass @@ -43,14 +61,14 @@ export const options = { }; const UNKEY_ROOT_KEY = __ENV.UNKEY_ROOT_KEY; -const KEY = __ENV.KEY; +const keys = __ENV.KEYS.split(","); if (!UNKEY_ROOT_KEY) { throw new Error("UNKEY_ROOT_KEY environment variable is required"); } -if (!KEY) { - throw new Error("KEY environment variable is required"); +if (keys.length === 0) { + throw new Error("KEYS environment variable is required"); } const headers = { @@ -58,30 +76,42 @@ const headers = { Authorization: `Bearer ${UNKEY_ROOT_KEY}`, }; -// biome-ignore lint/style/noDefaultExport: k6 needs a default export -export default function () { - const response = - Math.random() < 0.5 - ? http.post( - "https://api.unkey.dev/v1/keys.verifyKey", - JSON.stringify({ - key: KEY, - }), - { - headers: headers, - tags: { version: "v1" }, - }, - ) - : http.post( - "https://api.unkey.com/v2/keys.verifyKey", - JSON.stringify({ - key: KEY, - }), - { - headers: headers, - tags: { version: "v2" }, - }, - ); + +export function testV1KeyVerify() { + const key = keys[Math.floor(Math.random() * keys.length)]; + + const response = http.post( + "https://api.unkey.dev/v1/keys.verifyKey", + JSON.stringify({ + key: key, + }), + { + headers: headers, + tags: { version: "v1" }, + }, + ); + + check(response, { + "status is 200": (r) => r.status === 200, + }); + + requestLatencyTrend.add(response.timings.duration, { url: response.request.url }); +} + +export function testV2KeyVerify() { + const key = keys[Math.floor(Math.random() * keys.length)]; + + const response = http.post( + "https://api.unkey.com/v2/keys.verifyKey", + JSON.stringify({ + key: key, + }), + { + headers: headers, + tags: { version: "v2" }, + }, + ); + check(response, { "status is 200": (r) => r.status === 200, diff --git a/go/benchmarks/ratelimit.js b/go/benchmarks/ratelimit.js index e31f2c883f..7867db3d42 100644 --- a/go/benchmarks/ratelimit.js +++ b/go/benchmarks/ratelimit.js @@ -7,17 +7,16 @@ const requestLatencyTrend = new Trend("request_latency", true); const loadZones = [ "amazon:us:ashburn", // US East - // 'amazon:us:portland', // US West + "amazon:us:portland", // US West // 'amazon:ie:dublin', // Europe West - // 'amazon:de:frankfurt', // Europe Central + "amazon:de:frankfurt", // Europe Central // 'amazon:sg:singapore', // Asia Pacific - // 'amazon:jp:tokyo', // Asia Pacific East - // 'amazon:au:sydney', // Australia + "amazon:jp:tokyo", // Asia Pacific East + "amazon:au:sydney", // Australia // 'amazon:br:sao paulo', // South America - // 'amazon:in:mumbai', // India + "amazon:in:mumbai", // India // 'amazon:ca:montreal' // Canada ]; - const equalPercent = Math.floor(100 / loadZones.length); const distribution = {}; loadZones.forEach((zone, index) => { @@ -43,7 +42,6 @@ export const options = { }; const UNKEY_ROOT_KEY = __ENV.UNKEY_ROOT_KEY; -const REGION = __ENV.REGION || "local"; if (!UNKEY_ROOT_KEY) { throw new Error("UNKEY_ROOT_KEY environment variable is required"); @@ -61,38 +59,25 @@ export default function () { const identifier = identifiers[Math.floor(Math.random() * identifiers.length)]; + const body = JSON.stringify({ + namespace: "benchmark", + identifier, + limit: 1000, + duration: 60000, + }); + const response = Math.random() < 0.5 - ? http.post( - "https://api.unkey.dev/v1/ratelimits.limit", - JSON.stringify({ - namespace: "benchmark", - identifier, - limit: 100, - duration: 60000, - }), - { - headers: headers, - tags: { version: "v1", region: REGION }, - }, - ) - : http.post( - "https://api.unkey.com/v2/ratelimit.limit", - JSON.stringify({ - namespace: "benchmark", - identifier, - limit: 100, - duration: 60000, - }), - { - headers: headers, - tags: { version: "v2", region: REGION }, - }, - ); + ? http.post("https://api.unkey.dev/v1/ratelimits.limit", body, { + headers: headers, + }) + : http.post("https://api.unkey.com/v2/ratelimit.limit", body, { + headers: headers, + }); check(response, { "status is 200": (r) => r.status === 200, }); - requestLatencyTrend.add(response.timings.duration, { url: response.request.url, region: REGION }); + requestLatencyTrend.add(response.timings.duration, { url: response.request.url }); }