Error Handling
Ticksupply uses consistent error responses across all endpoints. This guide covers error formats, common error codes, and best practices for handling errors.
All errors follow this structure:
{
"error" : {
"code" : "not_found" ,
"message" : "Subscription sub_550e8400e29b41d4a716446655440000 not found"
}
}
When the server has structured context to share, the optional details field is included with free-form JSON:
{
"error" : {
"code" : "invalid_argument" ,
"message" : "Schema has too many columns" ,
"details" : {
"max_columns" : 100 ,
"provided" : 142
}
}
}
Field Description codeMachine-readable error code for programmatic handling messageHuman-readable description of the error detailsOptional object with additional, error-specific context. Omitted when absent — do not rely on its presence.
Every response also includes an X-Request-Id header (e.g., req_7f8e9d0c1b2a3456). Include this value when contacting support.
Match on code rather than message. Message text is for humans and may change; code is the stable contract.
Error codes reference
Code HTTP Status Description invalid_argument400 Request validation failed unauthenticated401 Missing or invalid API key permission_denied403 Valid key, but lacks permission not_found404 Resource doesn’t exist already_exists409 Resource already exists or idempotency conflict request_timeout408 Request did not complete within the 10-second deadline rate_limited429 Too many requests payment_required402 Billing-related denial (plan limit, trial cap, suspended account, or grace period restriction) internal500 Server error (contact support if persistent) unavailable503 Service temporarily unavailable
Handling errors by type
Authentication errors (401)
{
"error" : {
"code" : "unauthenticated" ,
"message" : "Invalid or missing API key"
}
}
Common causes:
Missing X-Api-Key header
Invalid or expired API key
Typo in the API key
Solution:
def handle_auth_error ( response ):
if response.status_code == 401 :
raise ValueError (
"Invalid API key. Check your key at "
"https://app.ticksupply.com/api-keys"
)
Validation errors (400)
{
"error" : {
"code" : "invalid_argument" ,
"message" : "Invalid datastream_id: must be a positive integer"
}
}
Common causes:
Missing required parameters
Invalid parameter types or values
Malformed request body
Solution:
def handle_validation_error ( response ):
if response.status_code == 400 :
error = response.json()[ "error" ]
raise ValueError ( f "Invalid request: { error[ 'message' ] } " )
Not found errors (404)
{
"error" : {
"code" : "not_found" ,
"message" : "Subscription sub_550e8400e29b41d4a716446655440000 not found"
}
}
Common causes:
Resource was deleted
Incorrect resource ID
Resource belongs to a different account
Solution:
def get_subscription_safe ( subscription_id ):
response = requests.get(
f "https://api.ticksupply.com/v1/subscriptions/ { subscription_id } " ,
headers = { "X-Api-Key" : API_KEY }
)
if response.status_code == 404 :
return None # Resource not found
response.raise_for_status()
return response.json()
Request timeout errors (408)
{
"error" : {
"code" : "request_timeout" ,
"message" : "Request exceeded 10s deadline"
}
}
Every endpoint has a hard 10-second deadline. If your request doesn’t finish within that window, you receive a 408 with the standard error envelope. This is rare under normal conditions — typical responses are tens of milliseconds — and usually indicates transient backend pressure.
Solution: Retry with exponential backoff. If the same endpoint times out repeatedly, contact support with the X-Request-Id.
def handle_timeout ( response , attempt , max_retries = 3 ):
if response.status_code == 408 and attempt < max_retries:
wait_time = 2 ** attempt # Exponential backoff
print ( f "Request timed out. Retrying in { wait_time } seconds..." )
time.sleep(wait_time)
return True # Should retry
return False
Heavy operations like creating an export return 202 Accepted immediately and finish asynchronously, so they never hit the 10-second deadline. Poll the resulting job for completion instead.
Rate limit errors (429)
{
"error" : {
"code" : "rate_limited" ,
"message" : "Rate limit exceeded. Retry after 30 seconds."
}
}
Headers included:
Retry-After: Seconds to wait before retrying
Solution:
def handle_rate_limit ( response ):
if response.status_code == 429 :
retry_after = int (response.headers.get( "Retry-After" , 60 ))
print ( f "Rate limited. Retrying in { retry_after } seconds..." )
time.sleep(retry_after)
return True # Should retry
return False
Billing errors (402)
{
"error" : {
"code" : "payment_required" ,
"message" : "You have reached the export limit for your trial period. Your full plan limits will be available when your trial ends."
}
}
Common causes:
Trial account has reached its stream or export size limit
Account is suspended for non-payment
Account is in a billing grace period with restricted actions
Plan does not include the requested action
Returned by: POST /v1/subscriptions, POST /v1/subscriptions/{id}/resume, and POST /v1/exports.
Solution: Retrying does not help — the underlying billing state must change. Upgrade your plan, update your payment method, or wait for your trial to convert in billing settings .
def handle_billing_error ( response ):
if response.status_code == 402 :
error = response.json()[ "error" ]
raise BillingError(
f " { error[ 'message' ] } "
"Update your plan at https://app.ticksupply.com/settings/billing"
)
Server errors (500, 503)
{
"error" : {
"code" : "internal" ,
"message" : "An internal error occurred"
}
}
Solution:
def handle_server_error ( response , retry_count = 0 , max_retries = 3 ):
if response.status_code >= 500 :
if retry_count < max_retries:
wait_time = 2 ** retry_count # Exponential backoff
print ( f "Server error. Retrying in { wait_time } seconds..." )
time.sleep(wait_time)
return True # Should retry
request_id = response.headers.get( "X-Request-Id" , "unknown" )
raise Exception (
f "Server error after { max_retries } retries. "
f "Request ID: { request_id } - "
f "Contact support@ticksupply.com"
)
return False
Complete error handling example
import time
import requests
class TicksupplyError ( Exception ):
def __init__ ( self , code , message , request_id = None ):
self .code = code
self .message = message
self .request_id = request_id
super (). __init__ ( f " { code } : { message } (request_id: { request_id } )" )
def api_request ( method , path , max_retries = 3 , ** kwargs ):
"""Make an API request with comprehensive error handling."""
url = f "https://api.ticksupply.com { path } "
headers = kwargs.pop( "headers" , {})
headers[ "X-Api-Key" ] = API_KEY
for attempt in range (max_retries + 1 ):
try :
response = requests.request(
method, url, headers = headers, timeout = 30 , ** kwargs
)
# Success
if response.status_code in ( 200 , 201 , 202 , 204 ):
if response.status_code == 204 :
return None
return response.json()
# Parse error response
try :
error = response.json().get( "error" , {})
except :
error = { "code" : "unknown" , "message" : response.text}
code = error.get( "code" , "unknown" )
message = error.get( "message" , "Unknown error" )
request_id = response.headers.get( "X-Request-Id" , "" )
# Rate limit - always retry
if response.status_code == 429 :
retry_after = int (response.headers.get( "Retry-After" , 30 ))
print ( f "Rate limited. Waiting { retry_after } s..." )
time.sleep(retry_after)
continue
# Server error - retry with backoff
if response.status_code >= 500 :
if attempt < max_retries:
wait_time = 2 ** attempt
print ( f "Server error. Waiting { wait_time } s..." )
time.sleep(wait_time)
continue
# Client errors - don't retry
raise TicksupplyError(code, message, request_id)
except requests.exceptions.Timeout:
if attempt < max_retries:
print ( f "Timeout. Retrying ( { attempt + 1 } / { max_retries } )..." )
continue
raise
except requests.exceptions.ConnectionError:
if attempt < max_retries:
time.sleep( 2 ** attempt)
continue
raise
raise Exception ( "Max retries exceeded" )
# Usage
try :
subscription = api_request( "GET" , f "/v1/subscriptions/ { sub_id } " )
print ( f "Found subscription: { subscription[ 'id' ] } " )
except TicksupplyError as e:
if e.code == "not_found" :
print ( "Subscription not found" )
elif e.code == "unauthenticated" :
print ( "Invalid API key" )
else :
print ( f "API error: { e } " )
Request tracing
Every response includes an X-Request-Id header:
X-Request-Id: req_7f8e9d0c1b2a3456
Log the X-Request-Id for all requests. When contacting support, include this ID to help us investigate issues quickly.
Best practices
Always check response status codes
Don’t assume requests succeed. Check status codes and handle errors appropriately.
Implement exponential backoff
For retryable errors (429, 5xx), use exponential backoff to avoid overwhelming the API.
Store X-Request-Id values in your logs for debugging and support requests.
Use idempotency keys for retries
When retrying mutating operations, use the same idempotency key to avoid duplicates.
Don't retry client errors
4xx errors (except 429) indicate issues with your request. Fix the request rather than retrying.
Getting help
When contacting support about errors:
Include the X-Request-Id header value from the response
Describe what you were trying to do
Include the request method and endpoint
Note the approximate time of the error
Email: support@ticksupply.com
Next steps
Rate Limiting Understand rate limits and how to handle them
Idempotency Safely retry requests using idempotency keys
Pagination Navigate large result sets efficiently
API Reference Explore the complete API documentation