Error Handling
PreflightAPI uses standard HTTP status codes and returns structured JSON error responses. Every error — whether generated by the APIM gateway (auth, rate limits, quotas, tier-gating) or the backend API (validation, not found, server errors) — uses the same response format.
Error Response Format
All errors return a unified ApiErrorResponse shape with a machine-readable code for programmatic handling and a traceId for support. Fields are omitted when null:
{
"code": "METAR_NOT_FOUND",
"message": "No current METAR available for station 'KXYZ'.",
"timestamp": "2026-01-15T18:56:00.0000000Z",
"traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"path": "/api/v1/metars/KXYZ"
}| Field | Type | Description | Always Present |
|---|---|---|---|
code | string | Machine-readable error code (e.g., METAR_NOT_FOUND, RATE_LIMIT_EXCEEDED) | Yes |
message | string | Human-readable description of what went wrong | Yes |
timestamp | string | ISO 8601 UTC timestamp of when the error occurred | Yes |
traceId | string | Correlation ID — include this when contacting support | Yes |
path | string | The request path that generated the error | Yes |
service | string? | Name of the unavailable external service (only on 503 backend responses) | No |
retryAfterSeconds | number? | Seconds to wait before retrying (only on RATE_LIMIT_EXCEEDED) | No |
quotaResetsAt | string? | ISO 8601 UTC timestamp when the monthly quota renews (only on QUOTA_EXCEEDED) | No |
validationErrors | object? | Field-level errors (only on VALIDATION_ERROR) | No |
details | string? | Stack trace (development environments only) | No |
Gateway Errors (401, 403, 429, 502)
These errors are generated by the APIM gateway before the request reaches the backend. They use the same ApiErrorResponse format as backend errors.
Authentication (401)
{
"code": "UNAUTHORIZED",
"message": "Access denied due to invalid subscription key. Make sure to provide a valid key for an active subscription.",
"timestamp": "2026-01-15T18:56:00.0000000Z",
"traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"path": "/api/v1/metars/KJFK"
}Tier-gating (403)
Returned when your subscription plan does not include access to the requested endpoint. See the endpoint access table for which endpoints are available on each plan.
{
"code": "TIER_RESTRICTED",
"message": "This endpoint is not available on the Student Pilot tier. Please upgrade to Private Pilot or higher.",
"timestamp": "2026-01-15T18:56:00.0000000Z",
"traceId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"path": "/api/v1/notams/KJFK"
}Rate limit exceeded (429)
Returned when you exceed your per-minute rate limit. Includes a retryAfterSeconds field and a standard Retry-After header.
{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please slow down and try again shortly.",
"retryAfterSeconds": 45,
"timestamp": "2026-01-15T18:56:00.0000000Z",
"traceId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"path": "/api/v1/metars/KJFK"
}Quota exceeded (429)
Returned when you exhaust your monthly API call quota. The quotaResetsAt field is an ISO 8601 UTC timestamp indicating when your quota renews.
{
"code": "QUOTA_EXCEEDED",
"message": "You have reached your monthly API call limit.",
"quotaResetsAt": "2026-03-15T06:00:00.0000000Z",
"timestamp": "2026-01-15T18:56:00.0000000Z",
"traceId": "d4e5f6a7-b8c9-0123-defa-234567890123",
"path": "/api/v1/metars/KJFK"
}Note
429. Use the code field to distinguish RATE_LIMIT_EXCEEDED (retryable after a short delay) from QUOTA_EXCEEDED (wait for monthly reset or upgrade).Backend unavailable (502)
Returned when the gateway cannot reach the backend (e.g. during a deploy, crash, or network issue). Safe to retry with backoff.
{
"code": "BACKEND_UNAVAILABLE",
"message": "The API backend is temporarily unavailable. Please try again shortly.",
"timestamp": "2026-01-15T18:56:00.0000000Z",
"traceId": "e5f6a7b8-c9d0-1234-efab-345678901234",
"path": "/api/v1/metars/KJFK"
}Backend Errors (400, 404, 409, 499, 500, 503)
Errors generated by the API backend use domain-specific error codes for precise programmatic handling. A few examples:
// 404 — resource not found
{
"code": "METAR_NOT_FOUND",
"message": "No current METAR available for station 'KXYZ'.",
"timestamp": "2026-01-15T18:56:00.0000000Z",
"traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"path": "/api/v1/metars/KXYZ"
}// 503 — external service unavailable (includes service field)
{
"code": "EXTERNAL_SERVICE_UNAVAILABLE",
"message": "The MagneticVariation service is temporarily unavailable.",
"service": "MagneticVariationService",
"timestamp": "2026-01-15T19:02:00.0000000Z",
"traceId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"path": "/api/v1/navlog"
}HTTP Status Codes
| Code | Name | Source | Description |
|---|---|---|---|
400 | Bad Request | Backend | Invalid query parameters, missing required fields, or invalid values. |
401 | Unauthorized | Gateway | Missing or invalid API key. Check the Ocp-Apim-Subscription-Key header. |
403 | Forbidden | Gateway | Endpoint not available on your plan (tier-gating). Upgrade to access this endpoint. |
404 | Not Found | Backend | The requested resource does not exist (e.g., unknown ICAO code, no METAR available). |
409 | Conflict | Backend | The request conflicts with the current state (e.g., duplicate resource). |
429 | Too Many Requests | Gateway | Rate limit or monthly quota exceeded. Check the code field to distinguish RATE_LIMIT_EXCEEDED from QUOTA_EXCEEDED. |
499 | Client Closed Request | Backend | The client disconnected before the server finished processing. No response body is sent. |
500 | Internal Server Error | Backend | An unexpected error occurred. Include the traceId when contacting support. |
502 | Bad Gateway | Gateway | Backend is unreachable (deploy, crash, or network issue). Safe to retry with backoff. |
503 | Service Unavailable | Backend | External data source down (service field identifies which). |
Error Codes Reference
The code field contains one of these machine-readable values. Use these to handle specific errors programmatically:
Gateway
UNAUTHORIZED | Missing or invalid API subscription key |
TIER_RESTRICTED | Endpoint not available on your subscription tier — upgrade to access |
RATE_LIMIT_EXCEEDED | Per-minute rate limit exceeded — includes retryAfterSeconds field |
QUOTA_EXCEEDED | Monthly call quota exhausted — includes quotaResetsAt field |
BACKEND_UNAVAILABLE | Backend is unreachable (deploy, crash, or network issue) |
General
INTERNAL_ERROR | An unexpected server error occurred |
VALIDATION_ERROR | One or more request parameters failed validation |
NOT_FOUND | Generic resource not found |
CONFLICT | Operation conflicts with current state |
Airports
AIRPORT_NOT_FOUND | No airport exists with the given identifier |
TERMINAL_PROCEDURE_NOT_FOUND | No terminal procedures available for this airport |
RUNWAY_NOT_FOUND | No runway found matching the query |
COMMUNICATION_FREQUENCY_NOT_FOUND | No communication frequency found for this airport |
Weather
METAR_NOT_FOUND | No current METAR available for the given station |
TAF_NOT_FOUND | No current TAF available for the given station |
WEATHER_SERVICE_UNAVAILABLE | NOAA weather service is temporarily unavailable |
WEATHER_DATA_MISSING | Weather data was expected but not available |
NOTAMs
NOTAM_NOT_FOUND | No NOTAM found matching the query |
NOTAM_SERVICE_UNAVAILABLE | FAA NOTAM service is temporarily unavailable |
Airspace & Obstacles
AIRSPACE_NOT_FOUND | No airspace found matching the query |
OBSTACLE_NOT_FOUND | No obstacle found matching the query |
NAVAIDs
NAVAID_NOT_FOUND | No navigation aid found matching the query |
Charts
CHART_SUPPLEMENT_NOT_FOUND | No chart supplement available for this airport |
Performance & Navigation
PERFORMANCE_CALCULATION_ERROR | Error calculating performance values |
INVALID_PERFORMANCE_DATA | Provided performance data is invalid or out of range |
NAVLOG_CALCULATION_ERROR | Error computing the nav log |
External Services
EXTERNAL_SERVICE_UNAVAILABLE | A downstream service is temporarily unavailable — the service field identifies which one |
Validation Errors
When a request fails validation (400 Bad Request), the response includes a validationErrors object with field-level details. Each key is the parameter name, and the value is an array of error messages. This applies to both explicit parameter validation (e.g. coordinates, radius) and automatic model binding errors (e.g. malformed JSON, invalid enum values):
{
"code": "VALIDATION_ERROR",
"message": "One or more validation errors occurred.",
"validationErrors": {
"latitude": [
"Latitude must be between -90 and 90 degrees"
],
"radiusNm": [
"Radius must be between 0 and 100 nautical miles"
]
},
"timestamp": "2026-01-15T18:56:00.0000000Z",
"traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"path": "/api/v1/notams/radius"
}Handling Errors
Since all errors use the same response shape, you only need a single parsing path. Switch on the code field for specific handling:
async function fetchMetar(icaoCode: string): Promise<Metar | null> {
const response = await fetch(
`https://api.preflightapi.com/api/v1/metars/${icaoCode}`,
{
headers: {
'Ocp-Apim-Subscription-Key': process.env.PREFLIGHT_API_KEY!,
},
},
)
if (!response.ok) {
const body = await response.json()
// Every error has: { code, message, timestamp, traceId, path }
switch (body.code) {
case 'UNAUTHORIZED':
throw new Error('Invalid API key')
case 'TIER_RESTRICTED':
throw new Error(body.message) // "...upgrade to Private Pilot..."
case 'METAR_NOT_FOUND':
return null // No METAR for this station — expected
case 'RATE_LIMIT_EXCEEDED':
// Retry after the indicated delay
throw new Error(`Rate limited. Retry in ${body.retryAfterSeconds}s`)
case 'QUOTA_EXCEEDED':
// Monthly quota exhausted — not retryable
throw new Error(`Quota exceeded. Resets at ${body.quotaResetsAt}`)
case 'BACKEND_UNAVAILABLE':
// Safe to retry with backoff
throw new Error(body.message)
case 'EXTERNAL_SERVICE_UNAVAILABLE':
// body.service identifies which service is down
throw new Error(`${body.message} (service: ${body.service})`)
default:
// Log traceId for support
console.error(`API error [${body.code}]: ${body.message}`)
console.error(`Trace ID: ${body.traceId}`)
throw new Error(body.message)
}
}
return response.json() as Promise<Metar>
}Retry Strategy
Not all errors are retryable. Here's a guide for which status codes to retry and which to handle immediately:
| Status | Code | Retryable? | Action |
|---|---|---|---|
400 | VALIDATION_ERROR | No | Fix the request — check validationErrors |
401 | UNAUTHORIZED | No | Check your API key is correct and present |
403 | TIER_RESTRICTED | No | Upgrade your plan to access this endpoint |
404 | *_NOT_FOUND | No | The resource doesn't exist — check the identifier |
409 | CONFLICT | No | Resolve the conflict |
429 | RATE_LIMIT_EXCEEDED | Yes | Wait for retryAfterSeconds, then retry |
429 | QUOTA_EXCEEDED | No | Wait for quotaResetsAt or upgrade plan |
500 | INTERNAL_ERROR | Maybe | Retry once — if it persists, contact support with traceId |
502 | BACKEND_UNAVAILABLE | Yes | Backend unreachable — retry with backoff |
503 | *_UNAVAILABLE | Yes | External service down — retry with backoff |
For a ready-to-use retry implementation, see the exponential backoff example on the rate limits page.