Error Handling
Error Response Format
All API errors follow a consistent JSON format with an error object containing a numeric code and a human-readable message. The HTTP status code always matches the code field in the response body. The Content-Type header for error responses is always application/json, even if the original request was for a streaming response (Luna endpoints). This makes it safe to always try parsing errors as JSON. The message field is designed to be developer-friendly. It describes what went wrong and, where possible, hints at how to fix the issue. These messages are stable and can be used for programmatic error handling (though we recommend matching on the status code, not the message text).
{
"error": {
"code": 401,
"message": "Invalid or missing API key. Include your key in the Ocp-Apim-Subscription-Key header."
}
}Common Error Codes
Here are the error codes you may encounter and what to do about each: 400 Bad Request -- The request body is malformed or missing required parameters. Common causes: invalid JSON syntax, missing the "query" field on search endpoints, passing a string where an array is expected (e.g., jurisdictions: "Netherlands" instead of ["Netherlands"]), or using an unsupported parameter value (e.g., sort_type: 5). Fix: check your request body against the API reference. 401 Unauthorized -- The API key is missing or invalid. Either the Ocp-Apim-Subscription-Key header is absent, or the key value does not match any active key. Fix: verify your header name and key value. Check that your key has not been rotated or deactivated. 403 Forbidden -- Your API key is valid but does not have permission to access this specific endpoint. This typically occurs when a Developer plan key tries to access Luna endpoints, which require a Professional or Enterprise plan. Fix: check your plan's feature access or upgrade. 404 Not Found -- The requested resource does not exist. For document retrieval, this means the DocumentIdentifier does not match any document. For Luna, this means the chat_id refers to an expired or invalid session. Fix: verify the identifier or reinitialize the Luna session. 429 Rate Limited -- You have exceeded your monthly request quota. The response includes a Retry-After header or the X-RateLimit-Reset header indicating when the quota resets. Fix: wait for the reset, implement caching to reduce requests, or upgrade your plan. 500 Internal Server Error -- An unexpected error on our side. These are rare and usually transient. Fix: retry after a short delay. If 500 errors persist for more than a few minutes, contact support@moonlit.ai.
Retry Strategies
Not all errors should be retried. Use this decision matrix: 400, 401, 403: Do NOT retry. These are client errors that require code changes or user action. Retrying will produce the same error. 404: Do NOT retry for document retrieval (the document does not exist). For Luna session errors, reinitialize the session and retry the question. 429: Retry after the cooldown period. Check the Retry-After header for the recommended wait time (in seconds). If no header is present, wait 60 seconds. Do not retry immediately -- you will get the same 429. 500: Retry with exponential backoff. Start with a 1-second delay, double it each attempt (1s, 2s, 4s), and give up after 3 retries. If you are seeing persistent 500 errors, check the Moonlit status page or contact support. Timeout / network errors: Retry with backoff, same as 500. Consider setting a request timeout of 30 seconds for search endpoints and 120 seconds for Luna streaming endpoints.
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response.json();
// Client errors: do not retry
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
const error = await response.json();
throw new Error(`Client error ${response.status}: ${error.error.message}`);
}
// Rate limited: wait and retry
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get("Retry-After") || "60");
console.log(`Rate limited. Waiting ${retryAfter}s...`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
// Server error: retry with backoff
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
console.log(`Server error ${response.status}. Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
continue;
}
throw new Error(`Server error ${response.status} after ${maxRetries} retries`);
} catch (err) {
if (err.message.startsWith("Client error")) throw err;
if (attempt === maxRetries) throw err;
const delay = Math.pow(2, attempt) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
}Error Logging Best Practices
Log the full error response along with the request context to make debugging fast. Include these fields in your error logs: - Timestamp (ISO 8601) - HTTP status code - Error message from the response body - The endpoint URL and HTTP method - Request parameters (the query, filters, and pagination -- but NOT the API key) - Request duration (how long until the error was received) - A correlation ID if your system uses request tracing Security: Do NOT log your API key. If you need to identify which key was used (e.g., in a multi-key setup), log only the last 4 characters. For production monitoring, track these metrics: - Error rate by status code (a sudden spike in 500s indicates an API issue) - Average response time by endpoint (latency degradation may precede errors) - Rate limit consumption (approaching 429 territory before it happens) - Luna session expiry rate (if high, consider extending your UI's idle timeout warning)
Graceful Degradation
For production applications, plan for API unavailability. Your application should degrade gracefully rather than crashing or showing raw error messages to users. Strategies: 1. Cache recent results: If the API is temporarily unavailable, serve cached results with a "Results may be outdated" banner. This is especially effective for filter endpoints and popular searches. 2. Queue and retry: For non-interactive workflows (e.g., daily alert scripts), queue failed requests and retry them when the API recovers rather than failing the entire batch. 3. User-friendly error messages: Map API error codes to user-friendly messages. A user should see "Search is temporarily unavailable. Please try again in a few minutes" -- not "500 Internal Server Error" or a raw JSON blob. 4. Circuit breaker: If you detect multiple consecutive failures (e.g., 5 errors in 60 seconds), stop making requests for a cooldown period. This prevents cascading failures and gives the API time to recover. 5. Monitoring and alerts: Set up automated alerts for elevated error rates so your team can investigate quickly. Use the Moonlit status page (status.moonlit.ai) to check for known incidents.
// User-friendly error mapping
function getUserMessage(statusCode) {
const messages = {
400: "Please check your search query and try again.",
401: "Authentication error. Please check your API configuration.",
403: "This feature is not available on your current plan.",
404: "The requested document could not be found.",
429: "You have reached your monthly search limit. Please try again next month or upgrade your plan.",
500: "Our search service is temporarily unavailable. Please try again in a few minutes.",
};
return messages[statusCode] || "An unexpected error occurred. Please try again.";
}