Error Handling
Understand error codes, status codes, and how to handle API errors gracefully.
HTTP Status Codes
| Status | Meaning | When |
|---|---|---|
200 | OK | Successful read or update |
201 | Created | Resource successfully created |
400 | Bad Request | Malformed JSON or invalid request structure |
401 | Unauthorized | Missing, invalid, or revoked API key |
403 | Forbidden | Key lacks permission for this operation |
404 | Not Found | Resource doesn't exist or is outside your organization |
409 | Conflict | Idempotency key conflict or duplicate resource |
422 | Unprocessable Entity | Request body failed validation |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Unexpected server error |
Error Response Format
All errors use a consistent envelope with one or more error objects:
{
"errors": [
{
"code": "VALIDATION_ERROR",
"message": "Field 'email' must be a valid email address",
"detail": { "field": "email" }
}
],
"meta": {
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
}| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
message | string | Human-readable description |
detail | object | Additional context (field names, limits, etc.) |
Error Codes
Authentication Errors (401)
| Code | Description |
|---|---|
API_KEY_REQUIRED | No Authorization header provided |
INVALID_API_KEY | Key not found or secret doesn't match |
API_KEY_REVOKED | Key has been revoked |
API_KEY_EXPIRED | Key has expired |
Permission Errors (403)
| Code | Description |
|---|---|
FORBIDDEN | API key permission level doesn't allow this method |
Validation Errors (422)
{
"errors": [
{
"code": "VALIDATION_ERROR",
"message": "Field 'email' must be a valid email address",
"detail": { "field": "email" }
},
{
"code": "VALIDATION_ERROR",
"message": "Field 'businessName' must be between 1 and 255 characters",
"detail": { "field": "businessName" }
}
]
}Multiple validation errors can be returned in a single response. Fix all reported issues before retrying.
Rate Limit Errors (429)
{
"errors": [
{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again in 45 seconds.",
"detail": {
"retryAfter": 45,
"limit": 1000,
"remaining": 0,
"resetAt": "2025-01-15T10:30:00Z"
}
}
]
}The Retry-After header is also set on the response. See Rate Limits for details.
Quota Errors (429)
{
"errors": [
{
"code": "MONTHLY_QUOTA_EXCEEDED",
"message": "Monthly API quota exceeded. Upgrade your plan for higher limits.",
"detail": {
"currentPlan": "basic",
"resetAt": "2025-02-01T00:00:00Z"
}
}
]
}Idempotency Errors
| Code | Status | Description |
|---|---|---|
INVALID_IDEMPOTENCY_KEY | 422 | Key format is invalid (must be 1-256 alphanumeric/hyphens/underscores) |
IDEMPOTENCY_CONFLICT | 409 | Another request with this key is currently in-flight |
Not Found (404)
{
"errors": [
{
"code": "NOT_FOUND",
"message": "Lead not found"
}
]
}Resources outside your organization also return 404 (not 403) to prevent information leakage.
Handling Errors in the SDK
The SDK throws typed errors that you can catch:
import { LendWorksClient, LendWorksError } from '@lendworks/sdk-node'
const client = new LendWorksClient({ apiKey: 'lw_live_xxxx' })
try {
const lead = await client.leads.create({
businessName: 'Acme Corp',
email: 'not-an-email',
})
} catch (error) {
if (error instanceof LendWorksError) {
console.error(`API Error: ${error.code} — ${error.message}`)
console.error(`Request ID: ${error.requestId}`)
console.error(`Status: ${error.status}`)
if (error.code === 'RATE_LIMIT_EXCEEDED') {
// Wait and retry
await new Promise(r => setTimeout(r, error.detail.retryAfter * 1000))
}
}
}Retry Strategy
| Status | Retry? | Notes |
|---|---|---|
400 | No | Fix your request |
401 | No | Check your API key |
403 | No | Check your key's permission level |
404 | No | Resource doesn't exist |
409 | No | Resolve the conflict |
422 | No | Fix validation errors |
429 | Yes | Wait for Retry-After seconds |
500 | Yes | Use exponential backoff with jitter |
For 500 errors, use exponential backoff:
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
if (error.status !== 500 || attempt === maxRetries) throw error
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000)
await new Promise(r => setTimeout(r, delay))
}
}
throw new Error('unreachable')
}Always include an Idempotency-Key when retrying write operations to prevent duplicate creates.