Skip to content

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

CodeMeaningDescription
200OKRequest succeeded
201CreatedResource created successfully
204No ContentRequest succeeded, no response body
400Bad RequestInvalid request format or parameters
401UnauthorizedMissing or invalid API key
403ForbiddenPlan doesn't include API access
404Not FoundResource doesn't exist
409ConflictResource conflict (e.g., duplicate slug)
422Unprocessable EntityValidation error
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer error (rare)

Error Response Format

All errors follow a consistent format:

json
{
  "detail": "Human-readable error message"
}

Authentication Errors

401 — Missing API Key

json
{
  "detail": "Missing API key. Include X-API-Key header."
}

Resolution: Include the X-API-Key header with a valid API key.

bash
curl -H "X-API-Key: klinky_live_your_api_key_here" \
  https://klinky-api.fly.dev/api/v1/public/links

401 — Invalid API Key

json
{
  "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

json
{
  "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

json
{
  "detail": "Invalid JSON in request body"
}

Resolution: Check your JSON syntax. Use a validator if needed.

422 — Missing Required Field

json
{
  "detail": "field required: name"
}

Resolution: Include all required fields in your request.

422 — Invalid Variant Weights

json
{
  "detail": "Variant weights must sum to 100, got 90"
}

Resolution: Adjust variant weights so they sum to exactly 100.

json
{
  "variants": [
    { "label": "a", "destination_url": "https://a.com", "weight": 50 },
    { "label": "b", "destination_url": "https://b.com", "weight": 50 }
  ]
}

422 — Invalid Geo Rules

json
{
  "detail": "geo_rules must contain a 'default' variant"
}

Resolution: Add a default key to your geo_rules:

json
{
  "geo_rules": {
    "US": "us_variant",
    "EU": "eu_variant",
    "default": "global_variant"
  }
}

422 — Invalid Variant Label in Geo Rules

json
{
  "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

json
{
  "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

json
{
  "detail": "API key not found"
}

Resolution: Verify the key ID using the list keys endpoint.

409 — Slug Already Taken

json
{
  "detail": "Slug 'homepage-test' is already taken"
}

Resolution: Choose a different slug or omit it to auto-generate one.

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

json
{
  "detail": "Rate limit exceeded. Please try again later."
}

Response Headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312800
Retry-After: 1800

Resolution: 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

python
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

javascript
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

  1. Log request details — Include URL, headers (without API key), and body in logs
  2. Check response headers — Rate limit headers help diagnose throttling
  3. Validate JSON — Use tools like jsonlint.com to check request body syntax
  4. Test with cURL — Simplify debugging by testing requests directly
  5. Check plan status — Verify your plan includes API access
  6. Monitor rate limits — Track your usage to avoid unexpected throttling

Support

If you encounter persistent errors:

  1. Check the status page for known issues
  2. Review request logs in your dashboard
  3. 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

Released under MIT License