Troubleshooting
The nine issues integrators hit most often — drawn from our support inbox and triage logs — with the actual error payloads and the fixes that resolve them. Covers the three most common HTTP error codes: 401 Unauthorized, 403 Forbidden, and 429 Too Many Requests, plus other frequent failure modes. If none of these match what you're seeing, email [email protected] with the request id from the response.
x-request-id header. Capturing and forwarding it cuts support resolution time by roughly 10x.401 UNAUTHORIZED or INVALID_API_KEY
The API didn't accept the key you sent. Walk this checklist top to bottom — it accounts for ~95% of these errors:
- Are you sending
Authorization: Bearer <your-key>exactly? Use this header format only. - Is the key trimmed? Stray newlines from .env files cause silent failures — log
key.lengthand confirm it matches the dashboard. - Was the key revoked? Check /dashboard/api-keys. A revoked key is permanently inactive — issue a new one.
- Are you pasting a masked value from the dashboard list? Existing keys are masked after creation for safety. Use the full key you copied when it was created, or create a new key and copy it before closing the dialog.
- Are you mixing live and test keys against the same environment? They are not interchangeable.
Tip: use the in-dashboard Self-Test tool
Before writing any code, click the Test button on any key row in Dashboard → API Keys. The tool fires a live authenticated request and shows the full request headers, response headers, status code, and response body in one panel — letting you confirm a key works (or see the exact error) without a terminal. Learn more in the Authentication guide.
نصيحة: قبل كتابة أي كود، انقر على زر اختبار في صف المفتاح بلوحة التحكم ← مفاتيح API. ستُطلِق الأداة طلبًا مُصادَقًا مباشرًا وتعرض ترويسات الطلب والاستجابة ورمز الحالة والنص الكامل للاستجابة — دون الحاجة إلى طرفية أو أي كود.
{ "error": { "code": "UNAUTHORIZED", "message": "Missing Authorization header. Send Authorization: Bearer <your-key> on every request." }}{ "error": { "code": "INVALID_API_KEY", "message": "Invalid API key — no matching live (kapi_live_) key found. The key may have been revoked, may belong to a different environment, or may be a typo." }}403 FORBIDDEN
A 403 response means your key was authenticated but the request was refused. There are three distinct causes — check the error.code field to identify which one applies:
SANDBOX_RESTRICTED — using a test key on a production-only endpoint
Sandbox keys (kapi_test_) cannot call endpoints that require live data or a paid plan. Generate a live key from Dashboard → API Keys.
{ "error": { "code": "SANDBOX_RESTRICTED", "message": "This endpoint requires a live API key. Generate a live key (kapi_live_) from the dashboard." }}IP_NOT_ALLOWED — request originates from a blocked IP
Enterprise organizations can restrict which IP addresses may use a key. If your request arrives from an IP not on the allowlist you'll get this error. Either send the request from an allowed server IP, or update the IP allowlist in your organization settings in the dashboard.
{ "error": { "code": "IP_NOT_ALLOWED", "message": "Authentication failed: Request IP is not in the organization allowlist for this API key." }}FORBIDDEN — endpoint or feature requires a higher plan
Some advanced features (e.g. CBUAE Open Finance, high-volume batch endpoints) are gated by plan. Check the endpoint's docs page for the minimum plan requirement, then upgrade your plan if needed.
{ "error": { "code": "FORBIDDEN", "message": "This feature requires a Professional plan or higher." }}ملاحظة: ثلاثة أسباب شائعة للخطأ 403: استخدام مفتاح اختباري مع نقطة نهاية تتطلب مفتاحًا حيًّا (SANDBOX_RESTRICTED)، أو الطلب من عنوان IP غير مُدرج في القائمة البيضاء للمؤسسة (IP_NOT_ALLOWED)، أو محاولة الوصول إلى ميزة تتطلب خطة أعلى (FORBIDDEN). تحقق من الحقل error.code لتحديد السبب الدقيق.
429 RATE_LIMIT_EXCEEDED
You've hit your plan's per-minute or per-month limit. The response always includes a Retry-After header (in seconds) — respect it instead of hammering. The standard X-RateLimit-* headers tell you exactly where you stand.
HTTP/1.1 429 Too Many RequestsRetry-After: 12X-RateLimit-Limit: 60X-RateLimit-Remaining: 0X-RateLimit-Reset: 1714324800 { "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded. Retry after 12 seconds." }}Production-grade clients should retry with exponential backoff and a cap. The official SDKs do this automatically:
// Exponential backoff with respect for Retry-Afterasync function callWithRetry(url, init, attempt = 0) { const res = await fetch(url, init) if (res.status !== 429 || attempt >= 4) return res const retryAfter = Number(res.headers.get("retry-after")) || 2 ** attempt await new Promise(r => setTimeout(r, retryAfter * 1000)) return callWithRetry(url, init, attempt + 1)}See Rate Limits for plan-by-plan numbers.
CORS error in the browser
KhaleejiAPI is a server-to-server API. Calling it directly from a browser is intentionally blocked by CORS — and even if it weren't, doing so would expose your API key to every site visitor.
The fix is the same in every framework: put a thin proxy on your backend that attaches the key, and let the browser call your proxy.
// ❌ Don't do this in the browser — your key will leak.fetch("https://khaleejiapi.dev/api/v1/ip/lookup?ip=8.8.8.8", { headers: { "Authorization": "Bearer kapi_live_..." },}) // ✅ Do this instead: call your own backend, which calls KhaleejiAPI.fetch("/api/proxy/ip-lookup?ip=8.8.8.8")Timeouts on AI endpoints
Standard endpoints (IP lookup, exchange rates, weather) reply in well under a second. AI-backed endpoints — translation, summarization, image processing — can take 3–8 seconds on a cold cache because they call upstream models. Default fetch timeouts of 5s will trip on those.
// Most KhaleejiAPI endpoints respond in <500ms.// AI endpoints (translate, summarize) can take 3–8s on cold cache.// Set a generous timeout for those.const controller = new AbortController()const timer = setTimeout(() => controller.abort(), 15_000)try { const res = await fetch(url, { signal: controller.signal, ...init })} finally { clearTimeout(timer)}If you consistently hit timeouts on cached endpoints, that's a problem on our side — file a ticket with the request id (see below).
Sporadic 5xx errors / “it worked yesterday”
Before assuming a regression, check status.khaleejiapi.dev and the upstream provider for the endpoint you called (we publish the upstream dependency for each API in its docs page).
When you contact support, include the x-request-id from the failing response. We index logs by that id and can usually pinpoint the cause within minutes.
// Every response includes an x-request-id header.// Capture it and include it when you contact support.const res = await fetch("https://khaleejiapi.dev/api/v1/health")console.log("request id:", res.headers.get("x-request-id"))400 VALIDATION_ERROR / MISSING_PARAM
A 400 response means the request was structurally valid but the parameters failed server-side validation. This is the third most common category in our support inbox — usually a missing query parameter or an incorrectly shaped request body.
- Check the endpoint's docs page for the required vs optional fields. Every endpoint lists them with their expected type and format.
GETendpoints take parameters as query strings (e.g.[email protected]).POSTendpoints take a JSON body withContent-Type: application/json.- Country and currency codes must be ISO formatted (e.g.
AEnotUAE,AEDnotDirham). - Date parameters follow ISO 8601:
YYYY-MM-DDorYYYY-MM-DDTHH:mm:ssZ.
{ "error": { "code": "VALIDATION_ERROR", "message": "Missing required parameter: email" }} // or for a body field:{ "error": { "code": "MISSING_PARAM", "message": "Request body must include 'amount' and 'currency'." }}Sandbox (test) key limitations
When you sign up, KhaleejiAPI issues a sandbox key (prefix kapi_test_). Sandbox keys let you explore the API without billing, but they differ from live keys in a few important ways that cause recurring support questions:
- Some endpoints return static mock data instead of live results (e.g. exchange rates, prayer times).
- Per-minute rate limits are tighter — 10 req/min versus 60+ on paid plans.
- A
403 SANDBOX_RESTRICTEDerror means the endpoint or feature you called requires a live key. Generate one in Dashboard → API Keys.
# Sandbox key prefix — issued automatically on free-plan signup.kapi_test_xxxxxxxxxxxxxxxxxxxx # Live key prefix — issued after email verification or from the dashboard.kapi_live_xxxxxxxxxxxxxxxxxxxx # Sandbox keys intentionally restrict some production-only features:# - Responses are mocked / rate-limited more aggressively.# - Certain endpoints (e.g. real exchange rates) may return static fixtures.# Always generate a live key from the dashboard for production use.If you're not sure which key you have, the prefix tells you: kapi_test_ = sandbox, kapi_live_ = production.
Webhook HMAC signature verification failures
Every webhook delivery includes an x-khaleeji-signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. The most common cause of verification failures is signing the re-serialized JSON body instead of the raw bytes.
JSON serialisation is not deterministic — key order and whitespace can differ between the sender and your parser, making the signatures diverge even though the data is identical.
import crypto from 'crypto' // ❌ Common mistake: re-serializing the parsed bodyconst reparsed = JSON.stringify(JSON.parse(rawBody)) // key-order and whitespace may differconst bad = crypto.createHmac('sha256', secret).update(reparsed).digest('hex') // ✅ Correct: sign the raw bytes as received, before any JSON.parse()// Express: use express.raw({ type: 'application/json' }) on this route// Next.js: use await request.text() — NOT request.json()const rawBody = await request.text()const expectedSig = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')const receivedSig = request.headers.get('x-khaleeji-signature') ?? '' // Always use timingSafeEqual to prevent timing attacksconst isValid = crypto.timingSafeEqual( Buffer.from(expectedSig, 'utf8'), Buffer.from(receivedSig, 'utf8'),)See the Webhooks guide for a full end-to-end verification example and the retry schedule.