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 StatusCodeDescriptionAction
400INVALID_IDMalformed ID in request pathCheck the resource ID
401UNAUTHORIZEDNo API key providedAdd X-API-Key header
401KEY_INVALIDKey not foundVerify the key value
401KEY_INACTIVEKey has been revokedContact support
401KEY_EXPIREDKey expiry date passedRequest a new key
403IP_NOT_ALLOWEDIP not on allowlistUpdate the IP allowlist
403SYMBOL_NOT_PERMITTEDFeed not in key's permissionsUpgrade plan or add feed access
404SYMBOL_NOT_FOUNDSymbol does not existCheck the symbol name
404NOT_FOUNDResource does not existCheck the URL path
405METHOD_NOT_ALLOWEDWrong HTTP methodUse GET
422INVALID_PARAMSMissing or invalid query parametersCheck the endpoint documentation
429RATE_LIMIT_EXCEEDEDRequest rate too highWait for Retry-After seconds
429TOO_MANY_CONNECTIONSWebSocket connection cap reachedReturned 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.
500SERVER_ERRORInternal server errorRetry with backoff
503SERVICE_UNAVAILABLEService temporarily downRetry 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);
  }
}