Docs POST /v1/verify

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
http
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

FieldTypeRequiredDescription
keystringYesThe customer's API key to verify (e.g., hld_live_xxx)
resourcestringNoResource being consumed (e.g., api_calls, chat_requests)
unitsintegerNoUnits to consume from quota (default: 1). Use 0 to check without consuming.
tenantIdstringNoYour 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

Success response
http
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

FieldTypeDescription
validbooleanWhether the key is valid and active
rateLimitobject | nullRate limit info (requests per minute)
rateLimit.remainingintegerRemaining requests in current window
rateLimit.limitintegerMaximum requests per window
rateLimit.resetintegerUnix timestamp when window resets
quotaobject | nullQuota info (requests per month)
quota.remainingintegerRemaining quota for billing period
quota.limitintegerTotal quota for billing period
quota.resetAtstringISO 8601 timestamp when quota resets
planstring | nullCustomer's current plan (e.g., "free", "pro", "business")
featuresstring[]List of enabled features for feature gating (e.g., ["model:gpt-4o", "priority-support"])
metadataobjectCustom 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.

Invalid key response
http
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:

HeaderDescription
X-RateLimit-LimitMaximum requests per rate limit window
X-RateLimit-RemainingRemaining requests in current window
X-RateLimit-ResetUnix timestamp when rate limit window resets
X-Quota-LimitTotal monthly quota limit
X-Quota-RemainingRemaining quota for billing period
X-Quota-ResetISO 8601 timestamp when quota resets
X-Holdify-PlanCustomer'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.

429 response
http
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.

402 response
http
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.

401 response
http
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

CodeHTTPDescription
INVALID_API_KEY401Project API key is invalid
API_KEY_EXPIRED401Customer API key has expired
API_KEY_DISABLED401Customer API key has been disabled
RATE_LIMIT_EXCEEDED429Per-minute rate limit exceeded
QUOTA_EXCEEDED402Monthly quota exhausted
BUDGET_EXCEEDED402Dollar-based budget exhausted
INVALID_REQUEST_BODY400Request body failed validation

Examples

cURL

curl
bash
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

api/route.ts
typescript
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.