Error Handling
The API uses standard HTTP status codes and returns structured error responses with actionable details.
Error response format
{
"error": {
"code": "INVALID_ADDRESS",
"message": "The destination address could not be validated",
"details": [
{ "field": "to.zip", "message": "Invalid ZIP code for state OR" }
]
}
}
| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable error code (see table below) |
error.message | string | Human-readable description |
error.details | array | Optional field-level validation errors |
error.details[].field | string | JSON path to the invalid field |
error.details[].message | string | What's wrong with the field |
HTTP status codes
| Code | Meaning | Retry? |
|---|---|---|
200 | Success | -- |
201 | Created | -- |
400 | Bad Request — invalid parameters | No — fix the request |
401 | Unauthorized — missing or invalid API key | No — check your API key |
403 | Forbidden — insufficient permissions | No — check your workspace permissions |
404 | Not Found | No — verify the resource ID |
409 | Conflict — duplicate idempotency key with different body | No — use a new idempotency key |
422 | Unprocessable Entity — valid JSON but business rule violation | No — see error details |
429 | Too Many Requests — rate limit exceeded | Yes — after retryAfter seconds |
500 | Internal Server Error | Yes — with exponential backoff |
502 | Bad Gateway — carrier API unavailable | Yes — after 30 seconds |
503 | Service Unavailable | Yes — after 60 seconds |
Error codes and resolution
| Code | HTTP | Description | How to fix |
|---|---|---|---|
INVALID_ADDRESS | 422 | Address validation failed | Verify street, city, state, and ZIP match. Use POST /v1/addresses/validate first. |
CARRIER_UNAVAILABLE | 502 | Carrier API is temporarily down | Retry after 30 seconds. If persistent, try a different carrier. |
RATE_NOT_FOUND | 404 | No rates for this lane/parcel | Check carrier supports the origin/destination pair. Verify parcel weight/dimensions. |
LABEL_ALREADY_CANCELLED | 409 | Label has already been voided | No action needed — label is already cancelled. |
WEIGHT_EXCEEDS_LIMIT | 422 | Parcel exceeds carrier weight limit | Reduce weight or use a carrier with higher limits (FedEx/UPS: 150 lb, USPS: 70 lb). |
CUSTOMS_REQUIRED | 422 | International shipment missing customs | Add a customs object with contentsType, items[] (description, quantity, value, HS code). |
INSUFFICIENT_BALANCE | 402 | Wallet balance too low | Fund your wallet in the Dashboard, or enable auto-recharge. |
INVALID_SERVICE | 400 | Carrier does not offer this service code | Use GET /v1/carriers/:code to list available services. |
DUPLICATE_SHIPMENT | 409 | Same idempotency key with different request body | Use a unique Idempotency-Key for each distinct request. |
DIMENSION_REQUIRED | 422 | Carrier requires dimensions for this service | Add lengthIn, widthIn, and heightIn to the parcel object. |
HAZMAT_NOT_SUPPORTED | 422 | Carrier/service does not support hazardous materials | Use a service that supports hazmat (e.g., FedEx Ground). |
VOID_WINDOW_EXPIRED | 422 | Label can no longer be cancelled | The carrier's void window has passed. Contact support for manual cancellation. |
Handling errors in SDKs
All FlexOps SDKs throw typed exceptions that map to error codes:
- Node.js
- Python
- C#
- Ruby
- PHP
- Go
- Java
import { FlexOpsClient, RateLimitError, FlexOpsError } from '@flexops/sdk';
try {
const label = await client.labels.create({ ... });
} catch (err) {
if (err instanceof RateLimitError) {
// Retry after err.retryAfter seconds
await sleep(err.retryAfter * 1000);
} else if (err instanceof FlexOpsError) {
console.error(`${err.code}: ${err.message}`);
// err.details contains field-level errors
}
}
from flexops import FlexOpsClient
from flexops.errors import RateLimitError, FlexOpsError
try:
label = client.labels.create(...)
except RateLimitError as e:
time.sleep(e.retry_after)
except FlexOpsError as e:
print(f"{e.code}: {e.message}")
try
{
var label = await client.PostAsync<LabelResponse>(
client.WsPath("shipping/labels"), request);
}
catch (FlexOpsRateLimitException ex)
{
await Task.Delay(TimeSpan.FromSeconds(ex.RetryAfter));
}
catch (FlexOpsException ex)
{
Console.WriteLine($"{ex.ErrorCode}: {ex.Message}");
}
begin
label = client.shipping.create_label(request)
rescue FlexOps::RateLimitError => e
sleep(e.retry_after)
rescue FlexOps::Error => e
puts "#{e.code}: #{e.message}"
end
try {
$label = $client->shipping->createLabel($request);
} catch (FlexOpsRateLimitError $e) {
sleep($e->getRetryAfter());
} catch (FlexOpsError $e) {
echo "{$e->getErrorCode()}: {$e->getMessage()}\n";
}
label, err := client.Shipping.CreateLabel(ctx, request)
if err != nil {
var rateLimitErr *flexops.RateLimitError
if errors.As(err, &rateLimitErr) {
time.Sleep(time.Duration(rateLimitErr.RetryAfter) * time.Second)
} else {
var flexErr *flexops.FlexOpsError
if errors.As(err, &flexErr) {
fmt.Printf("%s: %s\n", flexErr.Code, flexErr.Message)
}
}
}
try {
var label = client.post(client.wsPath("shipping/labels"),
request, LabelResponse.class);
} catch (FlexOpsRateLimitException e) {
Thread.sleep(e.getRetryAfter() * 1000L);
} catch (FlexOpsException e) {
System.out.printf("%s: %s%n", e.getErrorCode(), e.getMessage());
}