Error Handling
The Developer API uses standard HTTP status codes and returns JSON error bodies. Error responses are not wrapped in the data / meta envelope.
Error Response Format
Most errors carry a single error message:
{
"error": "Human-readable error message"
}
Validation error bodies vary by endpoint — read defensively:
| Where | Shape |
|---|---|
| Most endpoints (generic validation failure) | { "error": "Validation failed: ...", "errors": ["Name can't be blank"] } |
POST /api/v1/products | { "error": "Sku has already been taken", "errors": { "sku": ["has already been taken"] } } |
POST/PATCH /api/v1/customers | { "errors": ["Email is invalid"] } (no error key) |
| Webhook subscriptions | { "error": "Validation failed", "errors": ["Url must use HTTPS"] } |
Plan-related errors also include a machine-readable code, for example { "error": "Feature not enabled: enable_customers", "code": "feature_not_enabled" }.
Status Codes
Success
| Code | Meaning |
|---|---|
200 OK | Request succeeded |
201 Created | Resource created successfully |
207 Multi-Status | POST /api/v1/products/bulk_upsert — at least one row failed; see the body |
Client Errors
| Code | Meaning | Common Cause |
|---|---|---|
400 Bad Request | Missing required parameter: <name> | A required wrapper object (e.g. product, customer) is missing from the body |
401 Unauthorized | Invalid, expired or revoked API key | Check the X-API-Key header |
402 Payment Required | The store has no active POS subscription (code: pos_plan_required) or has not finished onboarding | Contact the store owner |
403 Forbidden | Missing scope, read-only or internal endpoint, a write with a Test Mode key, or a feature not included in the store's plan | Check key scopes and the error message |
404 Not Found | Resource doesn't exist | Check the ID |
409 Conflict | Record already exists (with a details field) | A unique value is already taken |
422 Unprocessable Entity | Validation failed or invalid input | Check error / errors |
429 Too Many Requests | Rate limit exceeded | Wait and retry |
Server Errors
| Code | Meaning |
|---|---|
500 Internal Server Error | Something went wrong on our end. The body is {"error": "An unexpected error occurred. Please contact support."} (or "Database error occurred" with a details field) |
Rate Limiting (429)
When you exceed your rate limit, the response looks like this:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 23
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1774180860
{"error":"Rate limit exceeded. Retry later."}
Retry-After is the number of seconds left in the current window, and X-RateLimit-Reset is the moment that window ends, as a Unix timestamp in seconds. Limits are counted in fixed one-minute windows.
Wait the number of seconds in Retry-After before retrying — that takes you into a new window. Add exponential backoff if repeated retries are still throttled.
Common Mistakes
Wrong header name
# Wrong — API keys use X-API-Key, not Authorization
curl -H "Authorization: Bearer bpos_abc..."
# Correct
curl -H "X-API-Key: bpos_abc..."
Authorization: Bearer <jwt> is used by the POS terminal for user session auth (JWT tokens). Sending an API key that way returns 401 Unauthorized. External API keys always use the X-API-Key header.
Missing scope
If you get 403 Forbidden with Insufficient scope. Required: <scope>, the key is missing that scope. For example, reading customers requires customers:read. Scopes cannot be added to an existing key — create a new key with the right scopes.
Write on read-only endpoint
Some endpoints only allow read access via API keys (e.g., sales). Any non-GET request to them returns 403 Forbidden with Write operations are not available for this endpoint via API key.