Troubleshooting

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.

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.length and 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. ستُطلِق الأداة طلبًا مُصادَقًا مباشرًا وتعرض ترويسات الطلب والاستجابة ورمز الحالة والنص الكامل للاستجابة — دون الحاجة إلى طرفية أو أي كود.

json
{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing Authorization header. Send Authorization: Bearer <your-key> on every request."
}
}
json
{
"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.

json
{
"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.

json
{
"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.

json
{
"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
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-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:

javascript
// Exponential backoff with respect for Retry-After
async 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.

javascript
// ❌ 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.

javascript
// 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.

javascript
// 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.
  • GET endpoints take parameters as query strings (e.g. [email protected]). POST endpoints take a JSON body with Content-Type: application/json.
  • Country and currency codes must be ISO formatted (e.g. AE not UAE, AED not Dirham).
  • Date parameters follow ISO 8601: YYYY-MM-DD or YYYY-MM-DDTHH:mm:ssZ.
json
{
"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_RESTRICTED error means the endpoint or feature you called requires a live key. Generate one in Dashboard → API Keys.
bash
# 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.

typescript
import crypto from 'crypto'
// ❌ Common mistake: re-serializing the parsed body
const reparsed = JSON.stringify(JSON.parse(rawBody)) // key-order and whitespace may differ
const 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 attacks
const 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.