Error Handling
Expect the unexpected. The Klinky API uses standard HTTP response codes and returns detailed error messages to help you diagnose and handle issues gracefully.
Test your integration — Free trial with full API access
HTTP Status Codes
| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created successfully |
| 204 | No Content | Request succeeded, no response body |
| 400 | Bad Request | Invalid request format or parameters |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | Plan doesn't include API access |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource conflict (e.g., duplicate slug) |
| 422 | Unprocessable Entity | Validation error |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error (rare) |
Error Response Format
All errors follow a consistent format:
{
"detail": "Human-readable error message"
}Authentication Errors
401 — Missing API Key
{
"detail": "Missing API key. Include X-API-Key header."
}Resolution: Include the X-API-Key header with a valid API key.
curl -H "X-API-Key: klinky_live_your_api_key_here" \
https://klinky-api.fly.dev/api/v1/public/links401 — Invalid API Key
{
"detail": "Invalid API key."
}Possible causes:
- Key was revoked
- Key was mistyped
- Key format is incorrect
Resolution: Verify your API key in the dashboard or create a new one.
Manage API keys — Create, list, and revoke keys
403 — Plan Without API Access
{
"detail": "Your plan (free) does not include API access. Upgrade to Growth or Scale."
}Resolution: Upgrade to a Growth or Scale plan in your account settings.
See pricing — Compare plans and API limits
Validation Errors
422 — Invalid JSON
{
"detail": "Invalid JSON in request body"
}Resolution: Check your JSON syntax. Use a validator if needed.
422 — Missing Required Field
{
"detail": "field required: name"
}Resolution: Include all required fields in your request.
422 — Invalid Variant Weights
{
"detail": "Variant weights must sum to 100, got 90"
}Resolution: Adjust variant weights so they sum to exactly 100.
{
"variants": [
{ "label": "a", "destination_url": "https://a.com", "weight": 50 },
{ "label": "b", "destination_url": "https://b.com", "weight": 50 }
]
}422 — Invalid Geo Rules
{
"detail": "geo_rules must contain a 'default' variant"
}Resolution: Add a default key to your geo_rules:
{
"geo_rules": {
"US": "us_variant",
"EU": "eu_variant",
"default": "global_variant"
}
}422 — Invalid Variant Label in Geo Rules
{
"detail": "Variant label 'us_variant' does not exist. Available variants: control, variant_b"
}Resolution: Use variant labels that exist in your variants array.
Resource Errors
404 — Link Not Found
{
"detail": "Link not found"
}Possible causes:
- Link was deleted
- Wrong link ID
- Link belongs to another user
Resolution: Verify the link ID using the list endpoint.
404 — API Key Not Found
{
"detail": "API key not found"
}Resolution: Verify the key ID using the list keys endpoint.
409 — Slug Already Taken
{
"detail": "Slug 'homepage-test' is already taken"
}Resolution: Choose a different slug or omit it to auto-generate one.
# Auto-generate slug
curl -X POST https://klinky-api.fly.dev/api/v1/public/links \
-H "X-API-Key: klinky_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Homepage Test",
"variants": [...]
}'Rate Limit Errors
429 — Rate Limit Exceeded
{
"detail": "Rate limit exceeded. Please try again later."
}Response Headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312800
Retry-After: 1800Resolution: Wait for the rate limit to reset. Use the Retry-After header to determine when to retry.
Learn rate limit strategies — Best practices for handling limits
Error Handling Examples
Python — Comprehensive Error Handling
import requests
import time
API_KEY = "klinky_live_your_api_key_here"
BASE_URL = "https://klinky-api.fly.dev/api/v1"
def make_request(method, endpoint, **kwargs):
"""Make API request with error handling."""
url = f"{BASE_URL}{endpoint}"
headers = kwargs.pop('headers', {})
headers['X-API-Key'] = API_KEY
try:
response = requests.request(method, url, headers=headers, **kwargs)
# Handle specific status codes
if response.status_code == 401:
raise AuthenticationError("Invalid or missing API key")
elif response.status_code == 403:
raise PermissionError("Plan doesn't include API access. Upgrade to Growth or Scale.")
elif response.status_code == 404:
raise ResourceNotFoundError(response.json().get('detail', 'Resource not found'))
elif response.status_code == 409:
detail = response.json().get('detail', '')
if 'already taken' in detail:
raise ConflictError(f"Resource conflict: {detail}")
raise ConflictError(detail)
elif response.status_code == 422:
detail = response.json().get('detail', 'Validation error')
raise ValidationError(detail)
elif response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
raise RateLimitError(f"Rate limit exceeded. Retry after {retry_after} seconds", retry_after)
elif response.status_code >= 500:
raise ServerError("Server error. Please try again later.")
# Raise for other 4xx errors
response.raise_for_status()
return response
except requests.exceptions.JSONDecodeError:
raise APIError(f"Invalid JSON response: {response.text}")
except requests.exceptions.RequestException as e:
raise APIError(f"Request failed: {e}")
# Custom exceptions
class APIError(Exception):
pass
class AuthenticationError(APIError):
pass
class PermissionError(APIError):
pass
class ResourceNotFoundError(APIError):
pass
class ConflictError(APIError):
pass
class ValidationError(APIError):
pass
class RateLimitError(APIError):
def __init__(self, message, retry_after):
super().__init__(message)
self.retry_after = retry_after
class ServerError(APIError):
pass
# Usage with retry logic
def create_link_with_retry(link_data, max_retries=3):
"""Create link with automatic retry on rate limit."""
for attempt in range(max_retries):
try:
return make_request('POST', '/public/links', json=link_data)
except RateLimitError as e:
if attempt < max_retries - 1:
print(f"Rate limited. Waiting {e.retry_after} seconds...")
time.sleep(e.retry_after)
else:
raise
except ConflictError as e:
# Handle slug conflict by generating new slug
if 'already taken' in str(e):
import uuid
link_data['slug'] = f"{link_data['slug']}-{uuid.uuid4().hex[:8]}"
print(f"Retrying with new slug: {link_data['slug']}")
else:
raise
# Example usage
try:
link_data = {
"name": "Test Link",
"slug": "test-link",
"variants": [
{"label": "a", "destination_url": "https://a.com", "weight": 50},
{"label": "b", "destination_url": "https://b.com", "weight": 50}
]
}
response = create_link_with_retry(link_data)
print(f"Created: {response.json()}")
except AuthenticationError:
print("Error: Check your API key")
except PermissionError:
print("Error: Upgrade your plan for API access")
except ValidationError as e:
print(f"Validation error: {e}")
except APIError as e:
print(f"API error: {e}")JavaScript — Error Handling
const API_KEY = 'klinky_live_your_api_key_here';
const BASE_URL = 'https://klinky-api.fly.dev/api/v1';
class APIError extends Error {
constructor(message, status, data) {
super(message);
this.status = status;
this.data = data;
}
}
class AuthenticationError extends APIError {}
class PermissionError extends APIError {}
class ResourceNotFoundError extends APIError {}
class ConflictError extends APIError {}
class ValidationError extends APIError {}
class RateLimitError extends APIError {
constructor(message, status, data, retryAfter) {
super(message, status, data);
this.retryAfter = retryAfter;
}
}
async function makeRequest(endpoint, options = {}) {
const url = `${BASE_URL}${endpoint}`;
const headers = {
'X-API-Key': API_KEY,
...options.headers
};
try {
const response = await fetch(url, { ...options, headers });
const data = await response.json().catch(() => null);
if (!response.ok) {
const detail = data?.detail || 'Unknown error';
switch (response.status) {
case 401:
throw new AuthenticationError(detail, response.status, data);
case 403:
throw new PermissionError(detail, response.status, data);
case 404:
throw new ResourceNotFoundError(detail, response.status, data);
case 409:
throw new ConflictError(detail, response.status, data);
case 422:
throw new ValidationError(detail, response.status, data);
case 429:
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
throw new RateLimitError(detail, response.status, data, retryAfter);
default:
throw new APIError(detail, response.status, data);
}
}
return { response, data };
} catch (error) {
if (error instanceof APIError) throw error;
throw new APIError(error.message, 0, null);
}
}
// Usage with retry
async function createLinkWithRetry(linkData, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const { data } = await makeRequest('/public/links', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(linkData)
});
return data;
} catch (error) {
if (error instanceof RateLimitError && attempt < maxRetries - 1) {
console.log(`Rate limited. Waiting ${error.retryAfter}s...`);
await new Promise(r => setTimeout(r, error.retryAfter * 1000));
} else if (error instanceof ConflictError && error.data?.detail?.includes('already taken')) {
// Generate new slug
linkData.slug = `${linkData.slug}-${Math.random().toString(36).substr(2, 8)}`;
console.log(`Retrying with slug: ${linkData.slug}`);
} else {
throw error;
}
}
}
}
// Example usage
createLinkWithRetry({
name: 'Test Link',
slug: 'test-link',
variants: [
{ label: 'a', destination_url: 'https://a.com', weight: 50 },
{ label: 'b', destination_url: 'https://b.com', weight: 50 }
]
})
.then(link => console.log('Created:', link))
.catch(error => {
if (error instanceof AuthenticationError) {
console.error('Check your API key');
} else if (error instanceof PermissionError) {
console.error('Upgrade your plan for API access');
} else if (error instanceof ValidationError) {
console.error('Validation failed:', error.message);
} else {
console.error('Error:', error.message);
}
});Debugging Tips
- Log request details — Include URL, headers (without API key), and body in logs
- Check response headers — Rate limit headers help diagnose throttling
- Validate JSON — Use tools like jsonlint.com to check request body syntax
- Test with cURL — Simplify debugging by testing requests directly
- Check plan status — Verify your plan includes API access
- Monitor rate limits — Track your usage to avoid unexpected throttling
Support
If you encounter persistent errors:
- Check the status page for known issues
- Review request logs in your dashboard
- Contact support with:
- Request details (endpoint, method, timestamp)
- Error response received
- Your account email
Questions? We're here to help — Reach out for integration support