Zakat calculations and donations for Salla, Zid, and Shopify
Add an opt-in, server-backed Zakat estimate or a separate charitable donation flow to an e-commerce experience. The examples use the KhaleejiAPI calculator from Node.js/Next.js and keep its API key off the storefront.
1. Read current metal prices and calculate an estimate
The authenticated GET /api/v1/zakat/calculate endpoint requires an amount. Use amount=0 to retrieve the response's price fields without submitting a wealth amount, or pass an amount the customer has chosen to calculate. For gold, silver, or multiple asset classes, use POST with a non-empty assets array. Both methods require the finance:zakat scope.
curl "https://khaleejiapi.dev/api/v1/zakat/calculate?amount=0¤cy=AED" \ -H "Authorization: Bearer $KHALEEJI_API_KEY"Example multi-asset request (money values use the top-level currency; metal weight is in grams):
curl -X POST "https://khaleejiapi.dev/api/v1/zakat/calculate" \ -H "Authorization: Bearer $KHALEEJI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "currency": "AED", "assets": [ { "type": "cash", "value": 50000 }, { "type": "gold", "value": 0, "weight": 20 } ] }'Read data.calculation.eligible, zakatDue, and nisabThreshold together. The response's data.metalPrices contains gold and silver prices per gram, their currency, and fetchedAt. Prices can be cached or fall back to an estimate; do not promise a guaranteed real-time market quote. See the API reference and the multi-asset guide.
2. Keep the API key on your server
Store a key with the minimum finance:zakat scope in a server-only environment variable such as KHALEEJI_API_KEY. Never use NEXT_PUBLIC_, VITE_, theme JavaScript, checkout scripts, or mobile bundles for the secret. Add your application's customer authentication, CSRF defense, and per-user rate limit to the route before deploying it.
// app/api/zakat-estimate/route.tsimport { NextRequest, NextResponse } from "next/server" const currencies = new Set(["AED", "SAR", "QAR", "BHD", "OMR", "KWD", "USD", "EUR", "GBP"])const assetTypes = new Set([ "cash", "gold", "silver", "stocks", "business", "agriculture", "mutual_funds", "crypto", "real_estate",]) function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value)} export async function POST(request: NextRequest) { const apiKey = process.env.KHALEEJI_API_KEY const storeOrigin = process.env.STOREFRONT_ORIGIN if (!apiKey || !storeOrigin) { return NextResponse.json({ error: "Zakat service is not configured" }, { status: 503 }) } // This browser-facing route accepts requests only from the configured storefront. let expectedOrigin: string try { expectedOrigin = new URL(storeOrigin).origin } catch { return NextResponse.json({ error: "Zakat service is not configured" }, { status: 503 }) } if (request.headers.get("origin") !== expectedOrigin) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }) } let body: unknown try { body = await request.json() } catch { return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }) } if (!isRecord(body)) { return NextResponse.json({ error: "Invalid request" }, { status: 400 }) } const currency = typeof body.currency === "string" ? body.currency.toUpperCase() : "AED" if (!currencies.has(currency) || !Array.isArray(body.assets) || body.assets.length < 1 || body.assets.length > 20) { return NextResponse.json({ error: "Invalid currency or assets" }, { status: 400 }) } const assets = [] for (const item of body.assets) { if (!isRecord(item) || typeof item.type !== "string" || !assetTypes.has(item.type) || typeof item.value !== "number" || !Number.isFinite(item.value) || item.value < 0) { return NextResponse.json({ error: "Invalid asset" }, { status: 400 }) } if (item.weight !== undefined && (typeof item.weight !== "number" || !Number.isFinite(item.weight) || item.weight <= 0)) { return NextResponse.json({ error: "Invalid metal weight" }, { status: 400 }) } if (item.irrigated !== undefined && typeof item.irrigated !== "boolean") { return NextResponse.json({ error: "Invalid irrigation setting" }, { status: 400 }) } assets.push({ type: item.type, value: item.value, ...(item.weight === undefined ? {} : { weight: item.weight }), ...(item.irrigated === undefined ? {} : { irrigated: item.irrigated }), }) } // REQUIRED: add your app's customer-session/CSRF check and per-user rate limit. // Origin validation is not authentication. try { const response = await fetch("https://khaleejiapi.dev/api/v1/zakat/calculate", { method: "POST", headers: { Authorization: ["Bearer", apiKey].join(" "), "Content-Type": "application/json", }, body: JSON.stringify({ currency, assets }), signal: AbortSignal.timeout(5000), }) const result: unknown = await response.json() return NextResponse.json(result, { status: response.status }) } catch { return NextResponse.json({ error: "Zakat service unavailable" }, { status: 502 }) }}The storefront submits only to your application's backend. Collect financial details only with the customer's informed consent, transmit the minimum required data, and avoid logging request bodies.
// Storefront code calls your backend, never KhaleejiAPI directly.const response = await fetch("/api/zakat-estimate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ currency: "AED", assets: [{ type: "cash", value: customerEnteredCashWealth }], }),}) if (!response.ok) throw new Error("Could not calculate an estimate")const { data } = await response.json()showEstimate({ eligible: data.calculation.eligible, zakatDue: data.calculation.zakatDue, nisabThreshold: data.calculation.nisabThreshold, goldPerGram: data.metalPrices.goldPerGram, silverPerGram: data.metalPrices.silverPerGram, pricesFetchedAt: data.metalPrices.fetchedAt,})3. Adapt the checkout integration to each platform
Salla
Connect your Salla storefront or app to your own backend using a checkout customization supported by your current Salla app setup. Render the estimate only after a customer chooses to provide relevant financial information. Confirm the current Salla extension and webhook contract in its official developer documentation; this guide intentionally does not assume a particular SDK callback or API path.
Zid
Use an integration point available to your Zid app and store plan, then have your app call the same merchant-controlled backend route. Confirm the supported checkout customization and signed webhook format in current Zid documentation. Do not treat a cart update or an order total as the customer's personal zakatable wealth.
Shopify
A Shopify app can connect its storefront or supported checkout extension to your backend. An App Proxy may be suitable for storefront requests, but it does not by itself add UI to every hosted checkout. Use only checkout extension points available to the merchant's plan and your app's API version; never put the KhaleejiAPI key in a theme, pixel, or extension bundle.
4. Verify webhooks and handle retries safely
A webhook is for order lifecycle events, not a synchronous checkout estimate. Verify its signature against the unmodified request body using that platform's documented scheme before parsing or acting on it. The Shopify sample below illustrates Shopify's HMAC format; do not reuse it for Salla or Zid. For every provider, atomically commit the event ID under a unique constraint together with a durable retryable job. Acknowledge a delivery with 2xx only after that transaction commits; acknowledge duplicates only when their work is completed or durably queued, never merely because the event ID exists. Return 5xx if durable acceptance fails so the provider can retry. Workers must retry failed jobs and make order/payment effects idempotent, including after a crash between a side effect and marking the job complete.
import { createHmac, timingSafeEqual } from "node:crypto" export function verifyShopifyWebhook( rawBody: Buffer, signature: string | null, appSecret: string,): boolean { if (!signature || !/^[A-Za-z0-9+/]+={0,2}$/.test(signature)) return false const expected = createHmac("sha256", appSecret).update(rawBody).digest() const supplied = Buffer.from(signature, "base64") if (supplied.toString("base64") !== signature) return false return supplied.length === expected.length && timingSafeEqual(supplied, expected)} // In the webhook route, read request.arrayBuffer() before parsing JSON.// Verify x-shopify-hmac-sha256, then in one database transaction persist// x-shopify-event-id under a unique constraint AND a durable retryable job.// Return 2xx only after commit; acknowledge duplicates only if their work// is completed or durably queued. If the transaction fails, return 5xx for retry.// Workers must retry failed jobs and make order/payment effects idempotent.The API calculates an estimate only; it does not create a donation, alter an order, or collect payment. Use your platform's supported donation or line-item flow only after the customer explicitly opts in, and keep donation payment/refund records separate from the calculation request.
Production checklist
- Send
Authorization: Bearer <your-api-key>only from a server-side integration. - Use the actual POST contract:
{ currency, assets: [{ type, value, weight? }] }. - Validate inputs, authenticate the customer, rate-limit the proxy, and set a short upstream timeout.
- Show the nisab result and price timestamp; explain that estimates may use cached or fallback prices.
- Never infer personal wealth from cart value or add a donation without explicit customer consent.
- Verify webhook signatures; atomically persist event IDs and durable retryable jobs before 2xx; make worker effects idempotent.