Error Handling
Error Response Format
All API errors use a consistent JSON structure:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded (minute). Retry after 42 seconds."
}
}
The code field is machine-readable and stable across API versions. The message is human-readable and may change.
Complete Error Code Reference
| HTTP Status | Code | Description | Action |
|---|---|---|---|
| 400 | INVALID_ID | Malformed ID in request path | Check the resource ID |
| 401 | UNAUTHORIZED | No API key provided | Add X-API-Key header |
| 401 | KEY_INVALID | Key not found | Verify the key value |
| 401 | KEY_INACTIVE | Key has been revoked | Contact support |
| 401 | KEY_EXPIRED | Key expiry date passed | Request a new key |
| 403 | IP_NOT_ALLOWED | IP not on allowlist | Update the IP allowlist |
| 403 | SYMBOL_NOT_PERMITTED | Feed not in key's permissions | Upgrade plan or add feed access |
| 404 | SYMBOL_NOT_FOUND | Symbol does not exist | Check the symbol name |
| 404 | NOT_FOUND | Resource does not exist | Check the URL path |
| 405 | METHOD_NOT_ALLOWED | Wrong HTTP method | Use GET |
| 422 | INVALID_PARAMS | Missing or invalid query parameters | Check the endpoint documentation |
| 429 | RATE_LIMIT_EXCEEDED | Request rate too high | Wait for Retry-After seconds |
| 429 | TOO_MANY_CONNECTIONS | WebSocket connection cap reached | Returned on the HTTP upgrade, so your client sees Unexpected server response: 429, not a JSON frame. The response carries Retry-After, X-Ws-Connections-Limit and X-Ws-Connections-In-Use (match header names case-insensitively). Close unused connections, or pack more symbols per connection — see WebSocket. |
| 500 | SERVER_ERROR | Internal server error | Retry with backoff |
| 503 | SERVICE_UNAVAILABLE | Service temporarily down | Retry with backoff |
Python — Structured Error Handling
import time
import requests
class ForexAPIError(Exception):
def __init__(self, code: str, message: str, http_status: int):
self.code = code
self.message = message
self.status = http_status
super().__init__(f"[{http_status}] {code}: {message}")
def api_get(url: str, headers: dict, params: dict = None) -> dict:
"""
Make a GET request with full error handling and 429 retry.
Returns the parsed JSON body on success.
Raises ForexAPIError on API errors.
"""
try:
resp = requests.get(url, headers=headers, params=params, timeout=15)
except requests.Timeout:
raise ForexAPIError("TIMEOUT", "Request timed out", 0)
except requests.ConnectionError:
raise ForexAPIError("CONNECTION_ERROR", "Could not reach the server", 0)
if resp.status_code == 200:
return resp.json()
# Parse structured error body
try:
body = resp.json()
error = body.get("error", {})
code = error.get("code", "UNKNOWN")
msg = error.get("message", "Unknown error")
except Exception:
code = "PARSE_ERROR"
msg = resp.text
raise ForexAPIError(code, msg, resp.status_code)
def api_get_with_retry(url, headers, params=None, max_retries=3):
"""Same as api_get but retries on 429 and 5xx."""
for attempt in range(max_retries):
try:
return api_get(url, headers, params)
except ForexAPIError as e:
if e.status == 429:
# Read exact wait time from the response
resp = requests.get(url, headers=headers, params=params, timeout=15)
wait = int(resp.headers.get("Retry-After", 60))
print(f"Rate limited — waiting {wait}s (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
elif e.status >= 500:
wait = 2 ** attempt
print(f"Server error — retrying in {wait}s")
time.sleep(wait)
else:
raise # Don't retry on 4xx (except 429)
raise RuntimeError("Max retries exceeded")
# Usage
headers = {"X-API-Key": "fxd_your_key"}
try:
data = api_get_with_retry(
"https://api.pipdata.net/v1/history",
headers,
{"symbol": "WMMARKETS:ZERO:EURUSD", "resolution": "60", "countback": 100}
)
except ForexAPIError as e:
if e.code == "RATE_LIMIT_EXCEEDED":
print("Hit rate limit — slow down requests")
elif e.code == "KEY_EXPIRED":
print("API key expired — request a replacement")
elif e.code == "SYMBOL_NOT_PERMITTED":
print("Feed access denied — check key permissions")
else:
print(f"API error: {e}")
JavaScript — Structured Error Handling
class ForexAPIError extends Error {
constructor(code, message, status) {
super(`[${status}] ${code}: ${message}`);
this.code = code;
this.status = status;
}
}
async function apiGet(endpoint, params = {}, apiKey) {
const url = new URL(`https://api.pipdata.net${endpoint}`);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
let resp;
try {
resp = await fetch(url, {
headers: { "X-API-Key": apiKey },
signal: AbortSignal.timeout(15_000)
});
} catch (err) {
throw new ForexAPIError("CONNECTION_ERROR", err.message, 0);
}
if (resp.ok) return resp.json();
let code = "UNKNOWN", message = "Unknown error";
try {
const body = await resp.json();
code = body?.error?.code ?? code;
message = body?.error?.message ?? message;
} catch {}
throw new ForexAPIError(code, message, resp.status);
}
// With 429 retry
async function apiGetWithRetry(endpoint, params, apiKey, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await apiGet(endpoint, params, apiKey);
} catch (err) {
if (err.status === 429) {
const wait = (parseInt(err.message.match(/\d+/)?.[0]) || 60) * 1000;
console.log(`Rate limited — waiting ${wait / 1000}s`);
await new Promise(r => setTimeout(r, wait));
} else if (err.status >= 500) {
const wait = 2 ** attempt * 1000;
console.log(`Server error — retrying in ${wait / 1000}s`);
await new Promise(r => setTimeout(r, wait));
} else {
throw err; // Don't retry 4xx
}
}
}
throw new Error("Max retries exceeded");
}
// Usage
try {
const data = await apiGetWithRetry(
"/v1/history",
{ symbol: "WMMARKETS:ZERO:EURUSD", resolution: "60", countback: 100 },
"fxd_your_key"
);
console.log(`${data.t.length} bars received`);
} catch (err) {
if (err.code === "RATE_LIMIT_EXCEEDED") {
console.error("Slow down — rate limit hit");
} else if (err.code === "SYMBOL_NOT_PERMITTED") {
console.error("Feed access denied");
} else {
console.error("API error:", err.message);
}
}