Error Handling
The Banklyze API uses standard HTTP status codes and returns a consistent JSON error envelope on every failure. This guide covers the error format, status codes, error codes, retry strategies, and how to handle validation and rate limit errors.
Error Response Format
All error responses return a JSON body with three fields:
| Name | Type | Required | Description |
|---|---|---|---|
| detail | string | Required | Human-readable description of what went wrong. |
| code | string | Required | Machine-readable error code for programmatic handling (e.g. DEAL_NOT_FOUND). |
| errors | array | Required | Array of field-level validation errors. Empty array for non-validation errors. |
{
"detail": "Deal not found",
"code": "DEAL_NOT_FOUND",
"errors": []
}HTTP Status Codes
The API returns the following HTTP status codes. Successful responses use 2xx; client errors use 4xx; server errors use 5xx.
| Name | Type | Required | Description |
|---|---|---|---|
| 200 OK | success | Optional | The request succeeded. Returned for GET, PATCH, and most POST requests. |
| 201 Created | success | Optional | A new resource was created. Returned for POST /deals, POST /deals/{id}/documents, and similar creation endpoints. |
| 204 No Content | success | Optional | The request succeeded with no response body. Returned for DELETE requests. |
| 400 Bad Request | client error | Optional | The request was malformed or contained invalid JSON. Check your request body and Content-Type header. |
| 401 Unauthorized | client error | Optional | Missing or invalid API key. Include a valid X-API-Key header or Authorization: Bearer token. |
| 403 Forbidden | client error | Optional | Your API key does not have permission to perform this action. Check your key scope or contact your organization admin. |
| 404 Not Found | client error | Optional | The requested resource does not exist or is not accessible with your API key's organization scope. |
| 409 Conflict | client error | Optional | The request conflicts with the current state of a resource (e.g. a duplicate external reference). |
| 422 Unprocessable Entity | client error | Optional | The request body failed schema validation or a business rule (e.g. ruleset weights do not sum to 1.0). |
| 429 Too Many Requests | client error | Optional | You have exceeded your rate limit. Check the X-RateLimit-Reset header and retry after that timestamp. |
| 500 Internal Server Error | server error | Optional | An unexpected error occurred on the Banklyze side. These are safe to retry with exponential backoff. |
Error Codes
The code field in every error response contains a machine-readable string you can use in conditional logic. Error codes are stable across API versions.
| Name | Type | Required | Description |
|---|---|---|---|
| VALIDATION_ERROR | 400 / 422 | Optional | One or more request fields failed schema validation. Check the errors array for field-level details. |
| AUTH_REQUIRED | 401 | Optional | No authentication credential was provided. Include the X-API-Key header. |
| INVALID_API_KEY | 401 | Optional | The provided API key is invalid, expired, or has been revoked. |
| FORBIDDEN | 403 | Optional | The authenticated key does not have permission to perform this operation. |
| NOT_FOUND | 404 | Optional | Generic not-found code when no more specific code applies. |
| DEAL_NOT_FOUND | 404 | Optional | No deal with the given ID exists within your organization. |
| DOCUMENT_NOT_FOUND | 404 | Optional | No document (statement) with the given ID exists for this deal. |
| DUPLICATE_RESOURCE | 409 | Optional | A resource with the same unique identifier already exists (e.g. duplicate external_ref in /ingest). |
| RATE_LIMIT_EXCEEDED | 429 | Optional | Request rate limit hit. Wait until X-RateLimit-Reset before retrying. |
| QUOTA_EXCEEDED | 429 | Optional | Monthly API call or document quota exhausted. Upgrade your plan or wait for the monthly reset. |
| PROCESSING_FAILED | 422 | Optional | Document processing failed in the analysis pipeline. Common causes: corrupted PDF, no extractable text, or blank pages. |
| WEIGHT_SUM_INVALID | 422 | Optional | Ruleset factor weights do not sum to exactly 1.0. Adjust weights so they total 100%. |
| INVALID_DECISION | 422 | Optional | The decision value provided to POST /deals/{id}/decision is not one of: approve, decline, fund, review. |
Retry Strategy
429 or 5xx responses, wait before retrying and double the wait time on each subsequent failure. This prevents overwhelming the API during transient issues. A good starting point is 1s, 2s, 4s, 8s with a maximum of 4 retries.Only 429 Too Many Requests and 5xx server errors are safe to retry automatically. Never retry 4xx errors other than 429 — they indicate a problem with the request itself that will not resolve on retry.
import time
import requests
from requests.exceptions import HTTPError
BANKLYZE_API_KEY = "bk_your_api_key"
BASE_URL = "https://api.banklyze.com/v1"
def api_request_with_retry(method: str, path: str, **kwargs) -> dict:
"""Make a Banklyze API request with exponential backoff retry."""
url = f"{BASE_URL}{path}"
headers = {"X-API-Key": BANKLYZE_API_KEY, **kwargs.pop("headers", {})}
max_retries = 4
backoff = 1 # seconds
for attempt in range(max_retries):
response = requests.request(method, url, headers=headers, **kwargs)
if response.status_code == 429:
# Respect Retry-After if present, otherwise use exponential backoff
retry_after = response.headers.get("Retry-After")
wait = int(retry_after) if retry_after else backoff * (2 ** attempt)
print(f"Rate limited. Retrying in {wait}s (attempt {attempt + 1})")
time.sleep(wait)
continue
if response.status_code >= 500:
if attempt < max_retries - 1:
wait = backoff * (2 ** attempt)
print(f"Server error {response.status_code}. Retrying in {wait}s")
time.sleep(wait)
continue
response.raise_for_status()
return response.json()
raise RuntimeError("Max retries exceeded")
# Usage
deals = api_request_with_retry("GET", "/deals")#!/usr/bin/env bash
# Retry a curl request up to 4 times with exponential backoff
API_KEY="bk_your_api_key"
MAX_RETRIES=4
BACKOFF=1
for attempt in $(seq 1 $MAX_RETRIES); do
HTTP_STATUS=$(curl -s -o /tmp/response.json -w "%{http_code}" \
-H "X-API-Key: $API_KEY" \
"https://api.banklyze.com/v1/deals")
if [ "$HTTP_STATUS" -eq 200 ]; then
cat /tmp/response.json
exit 0
elif [ "$HTTP_STATUS" -eq 429 ]; then
echo "Rate limited. Waiting ${BACKOFF}s before retry $attempt/$MAX_RETRIES..."
sleep $BACKOFF
BACKOFF=$((BACKOFF * 2))
elif [ "$HTTP_STATUS" -ge 500 ]; then
echo "Server error $HTTP_STATUS. Waiting ${BACKOFF}s..."
sleep $BACKOFF
BACKOFF=$((BACKOFF * 2))
else
echo "Non-retryable error: $HTTP_STATUS"
cat /tmp/response.json
exit 1
fi
done
echo "Max retries exceeded"
exit 1Validation Errors
When the request body fails Pydantic schema validation, the API returns 422 Unprocessable Entity with a populated errors array. Each entry in the array describes one invalid field:
| Name | Type | Required | Description |
|---|---|---|---|
| loc | string[] | Optional | Path to the invalid field. The first element is the source ("body", "query", "path") and subsequent elements are the field name(s). |
| msg | string | Optional | Human-readable description of the validation failure. |
| type | string | Optional | Pydantic error type code (e.g. "missing", "string_too_short", "float_parsing"). |
{
"detail": "Validation failed",
"code": "VALIDATION_ERROR",
"errors": [
{
"loc": ["body", "business_name"],
"msg": "Field required",
"type": "missing"
},
{
"loc": ["body", "funding_amount_requested"],
"msg": "Input should be a valid number",
"type": "float_parsing"
}
]
}Iterate over errors to surface field-level messages in your application UI. The loc path lets you map each error back to the input field that caused it.
Rate Limiting
Every API response includes three rate limit headers. Monitor these headers to avoid hitting the limit:
| Name | Type | Required | Description |
|---|---|---|---|
| X-RateLimit-Limit | header | Optional | Maximum number of requests allowed per minute for your API key. |
| X-RateLimit-Remaining | header | Optional | Number of requests remaining in the current 60-second window. |
| X-RateLimit-Reset | header | Optional | Unix timestamp (seconds) when the current rate limit window resets and the remaining count returns to the limit. |
HTTP/2 429
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1741132800
Content-Type: application/json
{
"detail": "Rate limit exceeded. Try again in 23 seconds.",
"code": "RATE_LIMIT_EXCEEDED",
"errors": []
}import os
import time
import requests
def check_rate_limit_headers(response: requests.Response) -> None:
"""Log rate limit status from response headers."""
limit = response.headers.get("X-RateLimit-Limit")
remaining = response.headers.get("X-RateLimit-Remaining")
reset_ts = response.headers.get("X-RateLimit-Reset")
if limit and remaining and reset_ts:
reset_dt = time.strftime("%H:%M:%S", time.localtime(int(reset_ts)))
print(f"Rate limit: {remaining}/{limit} remaining, resets at {reset_dt}")
# Warn when running low
if int(remaining) < 10:
print("Warning: fewer than 10 requests remaining in this window")
resp = requests.get(
"https://api.banklyze.com/v1/deals",
headers={"X-API-Key": os.environ["BANKLYZE_API_KEY"]},
)
check_rate_limit_headers(resp)X-RateLimit-Limit header always reflects your current plan limit. Check your plan in the Banklyze dashboard under Settings → Billing.