POST /v1/verify
Verify an API key and check rate limits, quota, plan, and features.
The verify endpoint is the core of Holdify. Call it on every incoming request to:
- Check if the API key is valid and active
- Get the customer's plan and features for feature gating
- Check rate limits (per minute) and quota (per month)
- Optionally consume units from the quota
- Auto-create tenants for multi-tenant tracking
Request
POST /v1/verify
Authorization: Bearer <project_api_key>
Content-Type: application/json
{
"key": "hld_live_abc123...",
"resource": "chat_requests",
"units": 1,
"tenantId": "customer_123"
}Request body
| Field | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | The customer's API key to verify (e.g., hld_live_xxx) |
| resource | string | No | Resource being consumed (e.g., api_calls, chat_requests) |
| units | integer | No | Units to consume from quota (default: 1). Use 0 to check without consuming. |
| tenantId | string | No | Your customer's ID for multi-tenant tracking. Automatically creates the tenant if it doesn't exist. Useful for per-customer analytics and usage tracking. |
Response
200 OK - Valid key
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: 550e8400-e29b-41d4-a716-446655440000
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Reset: 1705324200
X-Quota-Limit: 150000
X-Quota-Remaining: 149998
X-Quota-Reset: 2025-02-01T00:00:00Z
X-Holdify-Plan: pro
{
"valid": true,
"rateLimit": {
"remaining": 599,
"limit": 600,
"reset": 1705324200
},
"quota": {
"remaining": 149998,
"limit": 150000,
"resetAt": "2025-02-01T00:00:00Z"
},
"plan": "pro",
"features": ["model:claude-sonnet", "model:gpt-4o", "priority-support"],
"metadata": {
"customerId": "cus_123",
"email": "[email protected]"
}
}Response fields
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the key is valid and active |
| rateLimit | object | null | Rate limit info (requests per minute) |
| rateLimit.remaining | integer | Remaining requests in current window |
| rateLimit.limit | integer | Maximum requests per window |
| rateLimit.reset | integer | Unix timestamp when window resets |
| quota | object | null | Quota info (requests per month) |
| quota.remaining | integer | Remaining quota for billing period |
| quota.limit | integer | Total quota for billing period |
| quota.resetAt | string | ISO 8601 timestamp when quota resets |
| plan | string | null | Customer's current plan (e.g., "free", "pro", "business") |
| features | string[] | List of enabled features for feature gating (e.g., ["model:gpt-4o", "priority-support"]) |
| metadata | object | Custom metadata attached to the API key |
200 OK - Invalid key
Note: Invalid keys return 200 with valid: false.
This allows you to handle invalid keys without exceptions.
HTTP/1.1 200 OK
Content-Type: application/json
{
"valid": false,
"rateLimit": null,
"quota": null,
"plan": null,
"features": [],
"metadata": {}
}Response Headers
Every successful verify response includes these headers for easy access to rate limit and quota info:
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum requests per rate limit window |
| X-RateLimit-Remaining | Remaining requests in current window |
| X-RateLimit-Reset | Unix timestamp when rate limit window resets |
| X-Quota-Limit | Total monthly quota limit |
| X-Quota-Remaining | Remaining quota for billing period |
| X-Quota-Reset | ISO 8601 timestamp when quota resets |
| X-Holdify-Plan | Customer's current plan |
These headers allow you to forward rate limit info to your customers without parsing the response body.
Error Responses
429 Rate Limit Exceeded
Returned when the per-minute rate limit is exceeded.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705324200
{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again in 60 seconds.",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"docs_url": "https://holdify.io/docs/errors#rate-limiting"
}402 Quota Exceeded
Returned when the monthly quota is exhausted.
HTTP/1.1 402 Payment Required
Content-Type: application/json
X-Quota-Limit: 150000
X-Quota-Remaining: 0
X-Quota-Reset: 2025-02-01T00:00:00Z
{
"code": "QUOTA_EXCEEDED",
"message": "Monthly quota exceeded. Resets on 2025-02-01.",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"docs_url": "https://holdify.io/docs/errors#rate-limiting"
}401 Unauthorized
Your project API key is invalid or missing. This is different from verifying an invalid customer key.
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"code": "INVALID_API_KEY",
"message": "The provided project API key is invalid or has been revoked",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"docs_url": "https://holdify.io/docs/errors#invalid-api-key"
}All error codes
| Code | HTTP | Description |
|---|---|---|
| INVALID_API_KEY | 401 | Project API key is invalid |
| API_KEY_EXPIRED | 401 | Customer API key has expired |
| API_KEY_DISABLED | 401 | Customer API key has been disabled |
| RATE_LIMIT_EXCEEDED | 429 | Per-minute rate limit exceeded |
| QUOTA_EXCEEDED | 402 | Monthly quota exhausted |
| BUDGET_EXCEEDED | 402 | Dollar-based budget exhausted |
| INVALID_REQUEST_BODY | 400 | Request body failed validation |
Examples
cURL
curl -X POST https://api.holdify.io/v1/verify \
-H "Authorization: Bearer hld_proj_live_your_project_key" \
-H "Content-Type: application/json" \
-d '{
"key": "hld_live_customer_key",
"resource": "api_calls",
"units": 1,
"tenantId": "customer_123"
}'TypeScript SDK
import { Holdify } from '@holdify/sdk';
const holdify = new Holdify({
apiKey: process.env.HOLDIFY_PROJECT_KEY,
});
async function handleRequest(apiKey: string, customerId: string) {
const result = await holdify.verify(apiKey, {
resource: 'chat_requests',
units: 1,
tenantId: customerId, // Auto-creates tenant if doesn't exist
});
if (!result.valid) {
return { error: 'Invalid API key', status: 401 };
}
// Check rate limit (per-minute)
if (result.rateLimit.remaining <= 0) {
return {
error: 'Rate limit exceeded',
status: 429,
retryAfter: result.rateLimit.reset,
};
}
// Check quota (per-month)
if (result.quota.remaining <= 0) {
return {
error: 'Monthly quota exceeded',
status: 402,
resetAt: result.quota.resetAt,
};
}
// Feature gating
if (!result.features.includes('model:gpt-4o')) {
return { error: 'Upgrade to Pro for GPT-4o access', status: 403 };
}
// Process the request...
return { success: true, plan: result.plan };
}Features vs Entitlements
In the API response, features represents
the capabilities enabled for a customer. These are sometimes called "entitlements" in documentation
and in the dashboard UI.
They are the same thing: Features/entitlements are string identifiers that you
define in your plan configuration (e.g., model:gpt-4o, priority-support). Use them to gate
access to premium features in your application.
Best practices
- Call verify on every request. Don't cache the result client-side. Holdify responses are fast (<50ms) and reflect real-time changes.
- Check both rateLimit and quota. Rate limits reset every minute, while quota resets monthly. Handle both cases in your API.
- Use features for feature gating. Check
features.includes('feature-name')to gate access to premium features. - Use tenantId for analytics. Pass your customer's ID to track usage per customer and get better insights in the dashboard.
- Handle gracefully on timeout. If Holdify is unreachable, decide whether to fail open (allow) or fail closed (deny) based on your use case.