Ludo API Not Working? — Complete Troubleshooting Guide with Diagnostic Flowcharts

API integrations fail in predictable ways. A missing Authorization header, a blocked WebSocket port, a rate limit hit, a CORS preflight that never returns — these are not exotic edge cases, they are the daily reality of API integration. This guide walks through every common failure mode systematically, with text-based diagnostic flowcharts you can follow without any tools beyond a terminal, curl commands to test each endpoint in isolation, annotated JSON error response examples that decode exactly what went wrong, and a health check endpoint implementation you can drop into your backend to catch issues before your users do.

Diagnostic Decision Tree — Follow This First

Before diving into individual error codes, run through this decision tree. Most API issues follow a predictable path, and this flowchart identifies the category of problem in under a minute.

START: Is the API reachable at all?

└─ curl -s -o /dev/null -w "%{http_code}" https://api.ludokingapi.site/v1/health

└─ Returns non-000? → YES → Continue below

└─ Returns 000 or timeout? → NO → Check: internet connection, DNS resolution, firewall/proxy blocking, VPN interference

└─ DNS failure? → Try ping api.ludokingapi.site and nslookup api.ludokingapi.site

└─ Connection refused? → Server may be down. Check status.ludokingapi.site

└─ SSL/TLS error? → Check system clock, CA certificates, try with -k flag to test


STEP 1 → Can you authenticate?

└─ curl -s -H "Authorization: Bearer YOUR_KEY" https://api.ludokingapi.site/v1/players/me

└─ 401 Unauthorized? → API key invalid or missing. Check env variable, no quotes around key

└─ 403 Forbidden? → Key exists but lacks permission for this endpoint. Check plan tier

└─ 200 OK? → Authentication works. Continue to STEP 2


STEP 2 → Can you reach the specific endpoint?

└─ 404 Not Found? → Wrong URL path. Verify route, check for missing /v1/ prefix

└─ 405 Method Not Allowed? → Wrong HTTP verb. POST vs GET mismatch

└─ 200 or 400? → Endpoint exists. Continue to STEP 3


STEP 3 → Are you hitting rate limits?

└─ 429 Too Many Requests? → Check Retry-After header. Implement exponential backoff

└─ No 429 but slow responses? → Check for hidden polling loops in your code


STEP 4 → Is the response data what you expect?

└─ 200 but empty/wrong data? → Check API version. /v1/ vs /v2/ may have different schemas

└─ 500 Internal Server Error? → Server-side issue. Retry with backoff, check status page


STEP 5 → WebSocket-specific (if using real-time features)

└─ Connection refused on WSS? → Firewall blocking port 443 for WebSocket, or proxy not configured for WS

└─ Connects but disconnects immediately? → JWT expired or malformed. Re-authenticate

└─ Messages not arriving? → Check subscription topic matches room code. Wrong namespace = silent failure


STEP 6 → Still stuck?

└─ Collect: X-Request-ID from response headers, full request URL, HTTP method, response body, timestamp

└─ Contact support via WhatsApp with these details for fastest resolution

CORS Errors — Preflight Failures and Header Mismatches

Cross-Origin Resource Sharing (CORS) errors are the most common integration blocker for browser-based Ludo clients. CORS is a browser security mechanism that prevents a web page from making requests to a different origin unless that origin explicitly allows it. If your Ludo game runs in a browser and your API is on a different domain, every request triggers a CORS preflight — an OPTIONS request that asks the server "do you trust my origin?"

The preflight failure pattern typically looks like this: your code sends a POST /api/rooms with a custom header like Authorization: Bearer {token}, and the browser first sends an OPTIONS request to the same URL. If the server doesn't respond with the right headers, the browser blocks the actual request and you see an error like Access-Control-Allow-Origin missing or Preflight response not successful.

Diagnosing CORS Issues with curl

Browsers show CORS errors in the console, but you can diagnose the underlying issue with curl. Send an OPTIONS request with the same headers your browser sends:

Bash
# Simulate a CORS preflight request to diagnose missing headers
# Replace YOUR_KEY and https://api.ludokingapi.site with your values

curl -s -X OPTIONS \
  -H "Origin: https://your-ludo-game.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type" \
  -H "Access-Control-Allow-Origin: https://your-ludo-game.example.com" \
  https://api.ludokingapi.site/v1/rooms \
  -i

# Expected successful response includes:
# HTTP/1.1 204 No Content
# Access-Control-Allow-Origin: https://your-ludo-game.example.com
# Access-Control-Allow-Methods: GET, POST, OPTIONS
# Access-Control-Allow-Headers: Authorization, Content-Type
# Access-Control-Max-Age: 86400

If the response is missing Access-Control-Allow-Origin, the server isn't configured to handle CORS for your origin. In development, a common mistake is serving your frontend from localhost:3000 while the API expects localhost (without a port). Be explicit about the full origin, including the port number.

Server-Side CORS Configuration Fix (Node.js)

Node.js
// ── CORS Middleware — Node.js / Express ─────────────────────
const cors = require('cors');

// Allowlist of trusted origins — NEVER use '*' in production
const allowedOrigins = [
  'https://ludogame.example.com',
  'https://staging.ludogame.example.com',
  'http://localhost:3000',        // local development only
  'http://localhost:5173',        // Vite dev server
];

const corsOptions = {
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, Postman, curl)
    if (!origin) return callback(null, true);
    if (allowedOrigins.includes(origin)) {
      return callback(null, true);
    }
    callback(new Error(`CORS policy violation: origin {origin} not allowed`));
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Authorization', 'Content-Type', 'X-Request-ID', 'X-Client-Version'],
  exposedHeaders: ['X-Request-ID', 'X-RateLimit-Remaining', 'X-RateLimit-Reset', 'Retry-After'],
  credentials: true,
  maxAge: 86400,   // Cache preflight response for 24 hours
};

app.use(cors(corsOptions));

// Handle preflight requests explicitly (some setups need this)
app.options('*', cors(corsOptions));

HTTP Status Codes — Annotated Error Response Examples

Every API error returns a structured JSON response with a machine-readable error code, a human-readable message, and optionally a details array. Understanding these fields helps you write better error handling and differentiate between errors that warrant a retry (transient server errors) and errors that require code changes (validation failures, auth issues).

Error Response Schema

{
  "success": false,
  "error": "ERROR_CODE",          // Machine-readable identifier
  "message": "Human readable",   // What went wrong
  "details": [...],             // Validation details (when applicable)
  "requestId": "req_abc123",    // Always include in support tickets
  "timestamp": "2026-03-21T10:30:00Z"
}

401 Unauthorized

JSON Example 401 Response
{
  "success": false,
  "error": "UNAUTHORIZED",
  "message": "Invalid or missing API key. Include 'Authorization: Bearer {YOUR_KEY}' header.",
  "details": [],
  "requestId": "req_4f8a2b1c9d3e",
  "timestamp": "2026-03-21T10:30:00Z"
}

Common causes: API key not set, key set with quotes in the header string (e.g., "Bearer sk_live_xxx" with literal quotes), key copied with a trailing newline from a config file, or environment variable not loaded (common in Node.js when using dotenv but forgetting to call config()).

403 Forbidden

JSON Example 403 Response
{
  "success": false,
  "error": "FORBIDDEN",
  "message": "Your current plan (Free) does not include access to tournament endpoints. Upgrade to Pro.",
  "details": [
    {
      "field": "endpoint",
      "issue": "Tournament management requires Pro plan or higher",
      "upgradeUrl": "https://ludokingapi.site/pricing"
    }
  ],
  "requestId": "req_7c9e6679e5f4",
  "timestamp": "2026-03-21T10:30:00Z"
}

Common causes: Accessing a Pro-tier endpoint with a Free-tier key, attempting to modify a resource owned by another player, or a JWT token that lacks the required scope for the operation.

429 Too Many Requests

JSON Example 429 Response
{
  "success": false,
  "error": "RATE_LIMIT_EXCEEDED",
  "message": "You have exceeded 100 requests per minute. Slow down and retry after the reset time.",
  "details": {
    "limit": 100,
    "window": "1 minute",
    "plan": "Free"
  },
  "requestId": "req_1a2b3c4d5e6f",
  "timestamp": "2026-03-21T10:30:00Z"
}

Always read the Retry-After HTTP response header alongside this body. The body tells you what happened; the header tells you how long to wait. Rate limits are per API key and per IP in most configurations.

500 Internal Server Error

JSON Example 500 Response
{
  "success": false,
  "error": "INTERNAL_SERVER_ERROR",
  "message": "An unexpected error occurred on our servers. This is usually transient — please retry.",
  "details": [],
  "requestId": "req_9z8y7x6w5v4u",
  "timestamp": "2026-03-21T10:30:00Z"
}

WebSocket Connection Failures — Diagnosis and Reconnection

WebSocket failures are harder to debug than HTTP errors because browsers don't surface low-level WebSocket close codes in a friendly way. A WebSocket connection closed message in the console could mean a dozen different things: server unreachable, TLS handshake failure, invalid token, room not found, or the server actively closing the connection due to a policy violation.

WebSocket Diagnostic Decision Tree

WS CONNECTION FAILURE — Where does it fail?

A. Connection refused / timeout immediately

→ Check: Is the WSS endpoint correct? (wss:// not ws://, port 443)

→ Try: curl -v --max-time 10 https://api.ludokingapi.site/v1/game

→ Check: Corporate firewall or VPN blocking port 443 WebSocket

B. Connection established, then immediately closed (code 1006)

→ Check: JWT token is valid and not expired. Re-authenticate before connecting

→ Check: room code in query param is valid and room is active

→ Check: Account is not banned or rate-limited at the WebSocket layer

C. Connection stays open but no messages arrive

→ Check: Did you send the subscription message after onopen fires?

→ Check: Is the room code in the subscription message correct and uppercase?

→ Check: Server-side — is the room in a state that emits events?

D. Intermittent disconnections every few minutes

→ Check: Server-side idle timeout — implement heartbeat ping every 25 seconds

→ Check: Load balancer dropping idle connections. Keepalive interval should match LB timeout

Health Check Endpoint — Implementation

A health check endpoint is your first line of defense against API failures. Instead of making a real API call and interpreting a potentially misleading error, your client periodically pings GET /v1/health. If it returns 200 with the expected schema, the API is operational. If it returns anything else, you know there's a problem before your game logic tries to make a game-critical call.

Node.js health.js — Health Check Endpoint
// ── Health Check Endpoint — Node.js / Express ───────────────
const express = require('express');
const router = express.Router();
const redis = require('redis');
const { pool } = require('./db');  // your database connection pool

async checkRedis() {
  try {
    const start = Date.now();
    await redisClient.ping();
    return { status: 'ok', latencyMs: Date.now() - start };
  } catch (err) {
    return { status: 'error', message: err.message };
  }
}

async checkDatabase() {
  try {
    const start = Date.now();
    await pool.query('SELECT 1');
    return { status: 'ok', latencyMs: Date.now() - start };
  } catch (err) {
    return { status: 'error', message: err.message };
  }
}

// GET /v1/health — Basic liveness check (fast)
router.get('/', (req, res) => {
  res.json({
    status: 'ok',
    service: 'ludoking-api',
    version: 'v1',
    timestamp: new Date().toISOString()
  });
});

// GET /v1/health/ready — Readiness check (validates all dependencies)
router.get('/ready', async (req, res) => {
  const [redisHealth, dbHealth] = await Promise.all([
    checkRedis(),
    checkDatabase()
  ]);

  const overall = (redisHealth.status === 'ok' && dbHealth.status === 'ok')
    ? 'ok'
    : 'degraded';

  const statusCode = overall === 'ok' ? 200 : 503;
  res.status(statusCode).json({
    status: overall,
    service: 'ludoking-api',
    version: 'v1',
    timestamp: new Date().toISOString(),
    dependencies: {
      redis: redisHealth,
      postgresql: dbHealth
    }
  });
});

// GET /v1/health/detailed — Verbose health for monitoring dashboards
router.get('/detailed', async (req, res) => {
  const checks = await Promise.allSettled([
    checkRedis(),
    checkDatabase()
  ]);

  const results = checks.map((check, i) => ({
    name: i === 0 ? 'redis' : 'postgresql',
    ...(check.status === 'fulfilled' ? check.value : { status: 'error', message: check.reason.message })
  }));

  const allHealthy = results.every(r => r.status === 'ok');
  res.status(allHealthy ? 200 : 503).json({
    status: allHealthy ? 'ok' : 'unhealthy',
    service: 'ludoking-api',
    version: 'v1',
    uptime: process.uptime(),
    memoryUsage: process.memoryUsage(),
    timestamp: new Date().toISOString(),
    dependencies: results
  });
});

module.exports = router;

Curl Testing Commands for Every Endpoint

These curl commands let you test each endpoint in isolation from the comfort of your terminal. Running these is faster than debugging through a browser or a mobile app, and the structured output makes it easy to spot deviations from expected behavior.

Bash curl-commands.sh
# ── Health Check Commands ───────────────────────────────────
# Basic liveness — should return 200 if server is running
curl -s https://api.ludokingapi.site/v1/health | jq .

# Readiness with dependency check — validates Redis and DB
curl -s https://api.ludokingapi.site/v1/health/ready | jq .

# Detailed health with memory and uptime
curl -s https://api.ludokingapi.site/v1/health/detailed | jq .

# ── Authentication Test ─────────────────────────────────────
# Replace YOUR_API_KEY with your actual key from the dashboard
curl -s -X GET https://api.ludokingapi.site/v1/players/me \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" | jq .

# ── Room Operations ─────────────────────────────────────────
# Create a room
curl -s -X POST https://api.ludokingapi.site/v1/rooms \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"maxPlayers": 4, "privacy": "public"}' | jq .

# Store the room code from the response above, then:
# ROOM_CODE="ABCD12"

# Get room info (validation check)
curl -s -X GET https://api.ludokingapi.site/v1/rooms/$ROOM_CODE \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .

# Join a room
curl -s -X POST https://api.ludokingapi.site/v1/rooms/$ROOM_CODE/join \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"playerId": "test_player_001", "playerName": "TestPlayer"}' | jq .

# Start the game (host only)
curl -s -X POST https://api.ludokingapi.site/v1/rooms/$ROOM_CODE/start \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .

# Close a room (host only)
curl -s -X DELETE https://api.ludokingapi.site/v1/rooms/$ROOM_CODE \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .

# ── Error Case Testing ──────────────────────────────────────
# Test 401 with missing key
curl -s -X GET https://api.ludokingapi.site/v1/players/me | jq .

# Test 401 with invalid key
curl -s -X GET https://api.ludokingapi.site/v1/players/me \
  -H "Authorization: Bearer invalid_key_123" | jq .

# Test 404 for non-existent room
curl -s -X GET https://api.ludokingapi.site/v1/rooms/NOTREAL \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .

# Test 429 by hitting rate limit intentionally
for i in {1..110}; do
  curl -s -o /dev/null -w "%{http_code}\n" \
    https://api.ludokingapi.site/v1/health \
    -H "Authorization: Bearer YOUR_API_KEY"
done

# Test 400 validation error
curl -s -X POST https://api.ludokingapi.site/v1/rooms \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"maxPlayers": 99}'  # maxPlayers > 4 should fail validation | jq .

Logging Patterns for Debugging in Production

Structured logging is the difference between spending hours reproducing a bug and identifying it in seconds from a dashboard. Every API request should generate a log entry with the request ID, method, path, response status, and latency. Here is a pattern for instrumenting your Node.js API client that captures everything you need without overwhelming your logs.

Node.js api-client-logging.js
// ── Structured API Client with Request Logging ─────────────
class LudoAPIClient {
  constructor(baseUrl, apiKey, options = {}) {
    this.baseUrl = baseUrl.replace('/$', '');
    this.apiKey = apiKey;
    this.defaultHeaders = {
      'Authorization': `Bearer {apiKey}`,
      'Content-Type': 'application/json',
      'User-Agent': 'ludo-game-client/1.0'
    };
  }

  async request(method, path, body = null, retries = 3) {
    const requestId = `req_{Date.now().toString(36)`;
    const url = `{this.baseUrl}/v1{path}`;
    const start = Date.now();

    for (let attempt = 0; attempt <= retries; attempt++) {
      try {
        const options = {
          method,
          headers: { ...this.defaultHeaders, 'X-Request-ID': requestId }
        };
        if (body) options.body = JSON.stringify(body);

        console.log(JSON.stringify({
          event: 'api_request',
          requestId,
          attempt: attempt + 1,
          method,
          path,
          url,
          body: body ? '[present]' : null
        }));

        const response = await fetch(url, options);
        const responseBody = await response.json();
        const latencyMs = Date.now() - start;
        const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');

        console.log(JSON.stringify({
          event: 'api_response',
          requestId,
          status: response.status,
          latencyMs,
          rateLimitRemaining,
          responseSuccess: responseBody.success,
          errorCode: responseBody.error || null
        }));

        // Automatic retry for transient errors
        if (response.status === 429 && attempt < retries) {
          const retryAfter = parseInt(response.headers.get('Retry-After') || '2');
          const backoff = retryAfter * 1000 * Math.pow(2, attempt);
          console.log(JSON.stringify({
            event: 'api_retry', requestId, attempt: attempt + 1,
            waitingMs: backoff, reason: 'rate_limit'
          }));
          await new Promise(r => setTimeout(r, backoff));
          continue;
        }

        if (response.status >= 500 && attempt < retries) {
          const backoff = 1000 * Math.pow(2, attempt);
          console.log(JSON.stringify({
            event: 'api_retry', requestId, attempt: attempt + 1,
            waitingMs: backoff, reason: 'server_error'
          }));
          await new Promise(r => setTimeout(r, backoff));
          continue;
        }

        return { response, data: responseBody, requestId, latencyMs };
      } catch (err) {
        console.error(JSON.stringify({
          event: 'api_error',
          requestId,
          attempt: attempt + 1,
          error: err.message,
          stack: err.stack
        }));
        if (attempt === retries) throw err;
      }
    }
  }

  // Convenience methods
  get(path) { return this.request('GET', path); }
  post(path, body) { return this.request('POST', path, body); }
  delete(path) { return this.request('DELETE', path); }
}

// Usage:
const client = new LudoAPIClient(
  'https://api.ludokingapi.site',
  process.env.LUDOKING_API_KEY
);
const { data } = await client.get('/rooms/ABCD12');

Python Logging Setup with Structured JSON Logs

Python api_logger.py
# ── Python Structured API Logging ───────────────────────────
import logging
import json
import time
import requests
from typing import Optional, Dict, Any

# Configure JSON logging for production (use structlog or python-json-logger)
class JSONFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        log_data: Dict[str, Any] = {
            "timestamp": self.formatTime(record),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        if hasattr(record, "extra_fields"):
            log_data.update(record.extra_fields)
        return json.dumps(log_data)

logger = logging.getLogger("ludo_api")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)

class LudoAPIClient:
    BASE_URL = "https://api.ludokingapi.site/v1"

    def __init__(self, api_key: str, max_retries: int = 3):
        self.api_key = api_key
        self.max_retries = max_retries
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "User-Agent": "ludo-game-client-python/1.0"
        })

    def _log(self, level: str, event: str, **kwargs):
        getattr(logger, level.lower())(f"api.event={event}",
            extra={"extra_fields": kwargs})

    def request(self, method: str, endpoint: str,
                  data: Optional[Dict] = None) -> Dict:
        url = f"{self.BASE_URL}}{endpoint}"
        request_id = f"req_{int(time.time() * 1000):x}"
        start = time.time()

        for attempt in range(self.max_retries + 1):
            self._log("debug", "api_request", request_id=request_id,
                            attempt=attempt+1, method=method,
                            url=url, has_body=bool(data))

            response = self.session.request(
                method, url, json=data,
                headers={"X-Request-ID": request_id}
            )

            latency = (time.time() - start) * 1000
            rate_limit_remaining = response.headers.get("X-RateLimit-Remaining", "N/A")

            self._log("info", "api_response", request_id=request_id,
                            status=response.status_code, latency_ms=round(latency, 2),
                            rate_limit_remaining=rate_limit_remaining,
                            success=response.json().get("success", None),
                            error_code=response.json().get("error", None))

            # Retry on 429 and 5xx with exponential backoff
            if response.status_code == 429 and attempt < self.max_retries:
                retry_after = int(response.headers.get("Retry-After", "2"))
                backoff = retry_after * (2 ** attempt)
                self._log("warning", "api_retry",
                                request_id=request_id, waiting_s=backoff,
                                reason="rate_limit")
                time.sleep(backoff)
                continue

            if response.status_code >= 500 and attempt < self.max_retries:
                backoff = 2 ** attempt
                self._log("warning", "api_retry",
                                request_id=request_id, waiting_s=backoff,
                                reason="server_error")
                time.sleep(backoff)
                continue

            response.raise_for_status()
            return response.json()

        raise RuntimeError(f"Max retries exceeded for {method} {endpoint}{""}")

    def get_room(self, code: str) -> Dict:
        return self.request("GET", f"/rooms/{code.upper()}")

    def create_room(self, max_players: int = 4) -> Dict:
        return self.request("POST", "/rooms", {"maxPlayers": max_players})

Preflight Request Flow — Visual Breakdown

Why OPTIONS requests fail silently: When a browser sends a preflight (OPTIONS) request, it is looking for specific headers in the response. If your server doesn't respond to OPTIONS requests at all — returning a 404 or letting the request time out — the browser blocks the actual request without ever reaching your endpoint handler. The fix is to ensure your server handles OPTIONS requests, either by explicitly routing them or by using CORS middleware that intercepts them before they reach your route handlers.

The flow: Browser sends OPTIONS /v1/rooms with Origin, Access-Control-Request-Method, and Access-Control-Request-Headers → Server responds with 204 No Content and CORS headers → Browser sends the actual POST /v1/rooms with the Origin header → Server processes the request. If the OPTIONS response is missing or wrong, the POST never fires.

Quick test: curl -v -X OPTIONS https://api.ludokingapi.site/v1/rooms -H "Origin: https://yoursite.com" — look for Access-Control-Allow-Origin in the response headers. If it's missing, CORS is not configured for your origin.

Frequently Asked Questions

Still Stuck After Following This Guide?

Contact our support team with your X-Request-ID from the failing response. Most issues are resolved within hours when the right diagnostic information is provided.

Chat on WhatsApp