AI app builders

Use KhaleejiAPI with Lovable, v0, Bolt, Base44 and Cursor

Building a UAE or GCC app with an AI builder? Paste the prompt below and the builder will wire up UAE phone, email, Emirates ID and VAT validation, prayer times, Hijri dates and more, with your API key kept on the server.

1. Get a key (or try without one)

Sign up free for 1,000 requests per month, then copy your key from the dashboard. Email validation also works without a key for quick tests:

bash
# Try it with no account - email validation works without a key (rate-limited)
curl "https://khaleejiapi.dev/api/v1/email/[email protected]"

2. Paste this prompt into your builder

Change the last two lines to the feature you want.

text
Integrate KhaleejiAPI (https://khaleejiapi.dev) for UAE/GCC data.
Rules:
- Read https://khaleejiapi.dev/llms.txt and https://khaleejiapi.dev/openapi.json for endpoints.
- Base URL: https://khaleejiapi.dev/api/v1. Auth header: Authorization: Bearer <key>.
- Store the key as a server secret named KHALEEJI_API_KEY. Never put it in frontend code
or in a NEXT_PUBLIC_/VITE_ variable.
- Call KhaleejiAPI only from a backend function (Supabase Edge Function, Next.js route
handler, or serverless function). The frontend calls that function.
- Only proxy the specific endpoints this app needs (allowlist), not arbitrary paths.
- Authorize a verified session and enforce an atomic, shared-storage per-caller limit
before every upstream call. Fail closed if authorization or quota storage is unavailable.
- For anonymous signup, verify a server-side abuse challenge and enforce a trusted
network-based limit before calling upstream; never expose an unrestricted public proxy.
- Responses are { "data": ... } on success and { "error": { "code", "message" } } on failure.
Feature: validate the user's UAE mobile number on sign-up with GET /phone/validate?phone=...
and reject it unless data.valid is true, data.type is "mobile", and data.country.code === "AE".

3. Lovable, Base44, or Bolt with Supabase: Edge Function

These examples require a registered, signed-in user and an Upstash-compatible Redis REST store. Set the Redis URL and token as server secrets; all instances must share the store. The atomic counter permits 10 calls per user per hour across endpoints and fails closed on storage errors. Match limits to your plan and add an app-wide quota budget before launch to bound abuse across accounts. For pre-login signup, do not simply remove the session check: verify a single-use abuse challenge server-side, then rate-limit a network identifier from your hosting provider's trusted client-IP metadata (not a caller-supplied forwarding header) before fetching upstream.

typescript
// supabase/functions/khaleeji/index.ts (Lovable, Base44, Bolt + Supabase)
// Set the secret first: supabase secrets set KHALEEJI_API_KEY=... (or the project's Secrets UI)
// Also set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN as server secrets.
// SUPABASE_URL and SUPABASE_ANON_KEY are supplied by Supabase.
const ALLOWED = new Set(["phone/validate", "email/validate", "vat/validate", "validation/emirates-id"])
const cors = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
}
// Shared Redis REST storage: atomic limit of 10 calls per user per hour.
// Use the same store/prefix across instances and endpoints; never use an in-memory counter.
async function allowCaller(userId: string, redisUrl?: string, redisToken?: string) {
if (!redisUrl || !redisToken) throw new Error("Quota storage not configured")
const script = `local n = redis.call("INCR", KEYS[1])
if n == 1 then redis.call("EXPIRE", KEYS[1], 3600) end
return n`
const response = await fetch(redisUrl, {
method: "POST",
headers: { Authorization: "Bearer " + redisToken, "Content-Type": "application/json" },
body: JSON.stringify(["EVAL", script, "1", "khaleeji:caller:" + userId]),
cache: "no-store",
})
if (!response.ok) throw new Error("Quota storage unavailable")
const { result, error } = await response.json()
if (error || !Number.isSafeInteger(result) || result < 1) throw new Error("Invalid quota result")
return result <= 10
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: cors })
if (req.method !== "POST") return Response.json({ error: "method not allowed" }, { status: 405, headers: cors })
try {
// Verify with Auth; never trust a decoded JWT or a client-supplied user ID.
const authorization = req.headers.get("Authorization")
if (!authorization) return Response.json({ error: "sign in required" }, { status: 401, headers: cors })
const authResponse = await fetch(Deno.env.get("SUPABASE_URL") + "/auth/v1/user", {
headers: { Authorization: authorization, apikey: Deno.env.get("SUPABASE_ANON_KEY")! },
})
if (!authResponse.ok) return Response.json({ error: "invalid session" }, { status: 401, headers: cors })
const user = await authResponse.json()
if (typeof user.id !== "string" || !user.id || user.is_anonymous !== false) {
return Response.json({ error: "registered session required" }, { status: 401, headers: cors })
}
const { endpoint, params } = await req.json()
if (!ALLOWED.has(endpoint)) {
return Response.json({ error: "endpoint not allowed" }, { status: 400, headers: cors })
}
const url = new URL(`https://khaleejiapi.dev/api/v1/${endpoint}`)
for (const [k, v] of Object.entries(params ?? {})) url.searchParams.set(k, String(v))
if (!Deno.env.get("KHALEEJI_API_KEY")) throw new Error("API key not configured")
if (!await allowCaller(user.id, Deno.env.get("UPSTASH_REDIS_REST_URL"), Deno.env.get("UPSTASH_REDIS_REST_TOKEN"))) {
return Response.json({ error: "caller limit exceeded" }, { status: 429, headers: cors })
}
const res = await fetch(url, {
headers: { Authorization: `Bearer ${Deno.env.get("KHALEEJI_API_KEY")}` },
})
return new Response(await res.text(), {
status: res.status,
headers: { ...cors, "Content-Type": "application/json" },
})
} catch {
return Response.json({ error: "proxy unavailable" }, { status: 503, headers: cors })
}
})

Call it from the app:

typescript
// Frontend (React) - the key never reaches the browser
const { data, error } = await supabase.functions.invoke("khaleeji", {
body: { endpoint: "phone/validate", params: { phone: "+971501234567" } },
})
if (error || !data?.data?.valid || data.data.type !== "mobile" || data.data.country?.code !== "AE") {
setPhoneError("Please enter a valid UAE mobile number")
}

4. v0, Bolt, or Cursor with Next.js: route handler

Connect auth() to your app's Auth.js configuration and expose a stable user ID in the server-verified session. If your builder uses another auth provider, replace this with its server-side session verification, not a user ID supplied by the browser. Phone validation still requires the UAE country check shown above.

typescript
// app/api/khaleeji/phone/route.ts (v0, Bolt, Cursor - Next.js App Router)
// Add KHALEEJI_API_KEY in Vercel > Project > Settings > Environment Variables.
// Also add UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (server-only).
// Use your configured Auth.js auth() with a stable session.user.id.
import { NextResponse } from "next/server"
import { auth } from "@/lib/auth"
// Shared Redis REST storage: atomic limit of 10 calls per user per hour.
// Use the same store/prefix across instances and endpoints; never use an in-memory counter.
async function allowCaller(userId: string, redisUrl?: string, redisToken?: string) {
if (!redisUrl || !redisToken) throw new Error("Quota storage not configured")
const script = `local n = redis.call("INCR", KEYS[1])
if n == 1 then redis.call("EXPIRE", KEYS[1], 3600) end
return n`
const response = await fetch(redisUrl, {
method: "POST",
headers: { Authorization: "Bearer " + redisToken, "Content-Type": "application/json" },
body: JSON.stringify(["EVAL", script, "1", "khaleeji:caller:" + userId]),
cache: "no-store",
})
if (!response.ok) throw new Error("Quota storage unavailable")
const { result, error } = await response.json()
if (error || !Number.isSafeInteger(result) || result < 1) throw new Error("Invalid quota result")
return result <= 10
}
export async function GET(req: Request) {
try {
const session = await auth()
if (!session?.user?.id) return NextResponse.json({ error: "sign in required" }, { status: 401 })
const phone = new URL(req.url).searchParams.get("phone")
if (!phone) return NextResponse.json({ error: "phone is required" }, { status: 400 })
if (!process.env.KHALEEJI_API_KEY) throw new Error("API key not configured")
if (!await allowCaller(session.user.id, process.env.UPSTASH_REDIS_REST_URL, process.env.UPSTASH_REDIS_REST_TOKEN)) {
return NextResponse.json({ error: "caller limit exceeded" }, { status: 429 })
}
const res = await fetch(
`https://khaleejiapi.dev/api/v1/phone/validate?phone=${encodeURIComponent(phone)}`,
{ headers: { Authorization: `Bearer ${process.env.KHALEEJI_API_KEY}` }, cache: "no-store" },
)
return NextResponse.json(await res.json(), { status: res.status })
} catch {
return NextResponse.json({ error: "proxy unavailable" }, { status: 503 })
}
}
// Client component: fetch("/api/khaleeji/phone?phone=" + encodeURIComponent(value))

5. Cursor, Claude, Copilot: MCP server

Coding assistants can call KhaleejiAPI directly through npx @khaleejiapi/mcp-server. See the MCP server guide.

Related