Errors
Kanoniv uses RFC 7807 Problem Details for all error responses. Every error returns a structured JSON object with a consistent shape, making it straightforward to handle errors programmatically.
The error object
All errors return Content-Type: application/problem+json with this structure:
{
"type": "https://api.kanoniv.com/errors/validation-error",
"title": "Validation failed",
"status": 400,
"detail": "Field 'email' is required",
"instance": "/v1/ingest/batch",
"errors": [
{ "field": "email", "message": "required" }
]
}| Attribute | Type | Description |
|---|---|---|
type | string | A URI that identifies the error type. Use this for programmatic error handling. |
title | string | A short, human-readable summary of the problem type. |
status | integer | The HTTP status code. |
detail | string | A human-readable explanation specific to this occurrence. |
instance | string | The request path that generated the error. Optional. |
errors | array | Field-level validation errors. Only present on validation failures. |
Error types
Every error type URI maps to a specific category. Use the type suffix for programmatic handling.
| Type suffix | HTTP Status | Title | When it happens |
|---|---|---|---|
validation-error | 400 | Validation failed | Request body fails schema validation or business rules. |
bad-request | 400 | Bad request | Malformed request, missing required parameters. |
unauthorized | 401 | Unauthorized | Missing or invalid API key / JWT token. |
payment-required | 402 | Payment required | Trial has expired. Upgrade to continue. |
forbidden | 403 | Forbidden | Valid credentials but insufficient permissions (e.g., admin-only endpoint). |
not-found | 404 | Not found | The requested resource does not exist or does not belong to your tenant. |
conflict | 409 | Conflict | Resource already exists (e.g., duplicate webhook URL, duplicate source name). |
payload-too-large | 413 | Payload too large | Request body exceeds the size limit. |
quota-exceeded | 429 | Quota exceeded | Identity count or rate limit exceeded. Respect Retry-After header. |
internal-error | 500 | Internal server error | Something went wrong on our end. Retry with exponential backoff. |
HTTP status codes
Kanoniv uses standard HTTP status codes grouped into three ranges:
- 2xx - The request succeeded.
- 4xx - The request failed due to something on the client side. Fix the request and retry.
- 5xx - The request failed due to something on the server side. Retry with backoff.
| Code | Meaning |
|---|---|
200 | OK - Request succeeded. |
201 | Created - Resource was created. |
204 | No Content - Successful delete, no response body. |
400 | Bad Request - Invalid parameters or request body. |
401 | Unauthorized - Invalid or missing authentication. |
402 | Payment Required - Trial expired. |
403 | Forbidden - Insufficient permissions. |
404 | Not Found - Resource does not exist. |
409 | Conflict - Resource already exists. |
413 | Payload Too Large - Request body exceeds limit. |
429 | Too Many Requests - Rate or quota limit exceeded. |
500 | Internal Server Error - Retry with backoff. |
Handling errors
Python SDK
The Python SDK maps HTTP errors to typed exceptions. Catch specific types for targeted handling.
from kanoniv import (
KanonivError, # Base - catch all API errors
ValidationError, # 400 - bad request body
AuthenticationError, # 401 - invalid credentials
ForbiddenError, # 403 - insufficient permissions
NotFoundError, # 404 - resource not found
ConflictError, # 409 - duplicate resource
RateLimitError, # 429 - rate/quota limit hit
ServerError, # 5xx - server-side issue
)
try:
client.resolve_rt.realtime(
source_name="crm",
external_id="cus_123",
data={"email": "[email protected]"},
)
except NotFoundError:
print("Source not found - check the source name")
except RateLimitError as e:
print(f"Rate limited - retry after {e.retry_after}s")
except ValidationError as e:
print(f"Bad request: {e.detail}")
except KanonivError as e:
print(f"API error {e.status_code}: {e}")TypeScript SDK
The TypeScript SDK provides the same error hierarchy as Python, using class-based exceptions.
import {
KanonivClient,
KanonivError, // Base - catch all API errors
ValidationError, // 400 - bad request body
AuthenticationError, // 401 - invalid credentials
ForbiddenError, // 403 - insufficient permissions
NotFoundError, // 404 - resource not found
ConflictError, // 409 - duplicate resource
RateLimitError, // 429 - rate/quota limit hit
ServerError, // 5xx - server-side issue
} from '@kanoniv/sdk';
try {
await client.resolveRt.realtime({
sourceName: 'crm',
externalId: 'cus_123',
data: { email: '[email protected]' },
});
} catch (err) {
if (err instanceof NotFoundError) {
console.log('Source not found - check the source name');
} else if (err instanceof RateLimitError) {
console.log(`Rate limited - retry after ${err.retryAfter}s`);
} else if (err instanceof ValidationError) {
console.log(`Bad request: ${err.message}`);
} else if (err instanceof KanonivError) {
console.log(`API error ${err.statusCode}: ${err.message}`);
}
}Go SDK
The Go SDK uses sentinel errors with errors.Is() for matching specific error types.
import "errors"
result, err := client.ResolveRt.Realtime(ctx, &kanoniv.RealtimeResolveParams{
SourceName: "crm",
ExternalID: "cus_123",
Data: map[string]interface{}{"email": "[email protected]"},
})
if err != nil {
var apiErr *kanoniv.KanonivError
if errors.As(err, &apiErr) {
switch {
case errors.Is(err, kanoniv.ErrNotFound):
fmt.Println("Source not found - check the source name")
case errors.Is(err, kanoniv.ErrRateLimit):
fmt.Printf("Rate limited - retry after %.0fs\n", apiErr.RateLimitInfo())
case errors.Is(err, kanoniv.ErrValidation):
fmt.Printf("Bad request: %s\n", apiErr.Message)
default:
fmt.Printf("API error %d: %s\n", apiErr.StatusCode, apiErr.Message)
}
}
}curl
Check the HTTP status code and parse the JSON body:
curl -s -w "\n%{http_code}" https://api.kanoniv.com/v1/entities/nonexistent \
-H "X-API-Key: kn_..." | {
read body
read code
if [ "$code" -ne 200 ]; then
echo "Error $code: $(echo $body | jq -r '.detail')"
fi
}Validation errors
When a request body fails validation, the response includes an errors array with per-field details:
{
"type": "https://api.kanoniv.com/errors/validation-error",
"title": "Validation failed",
"status": 400,
"detail": "Request body has validation errors",
"errors": [
{ "field": "url", "message": "URL must use HTTPS" },
{ "field": "event_types", "message": "At least one event type is required" }
]
}Rate limiting
When you exceed the rate limit, the response includes a Retry-After header. The Python SDK respects this automatically.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/problem+json
{
"type": "https://api.kanoniv.com/errors/quota-exceeded",
"title": "Quota exceeded",
"status": 429,
"detail": "Rate limit exceeded. Retry after 30 seconds."
}Rate limit headers are included on every response:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1707134400Best practices
Handle errors by type, not status code. The
typefield is stable and specific. Status codes are shared across error types (e.g., bothvalidation-errorandbad-requestreturn 400).Retry 5xx errors with exponential backoff. Server errors are transient. Use jitter to avoid thundering herd.
Never retry 4xx errors without changing the request. Client errors indicate a problem with your request - retrying the same request will fail again.
Log the full error object for debugging. The
detailfield often contains the specific issue. For 5xx errors, include theinstancefield when contacting support.Use idempotency keys for safe retries. When retrying POST requests, include an
Idempotency-Keyheader to prevent duplicate operations. See Idempotency.
