E-commerce integrations

Shopify and WooCommerce checkout validation

Validate GCC phone numbers and VAT/TRN numbers in Shopify storefront/cart flows and WooCommerce classic checkout, and preview GCC VAT amounts, without exposing your API key. Every example calls KhaleejiAPI from your server only.

What this guide covers

  • GET /api/v1/phone/validate?phone= (scope validation:phone): GCC numbers in international format; returns valid, type, country, and formats.e164 when valid.
  • GET /api/v1/vat/validate?trn=&country= (scope validation:vat): format and, where published, check-digit validation of GCC VAT/TRN numbers. It does not confirm registration with the tax authority.
  • POST /api/v1/vat/calculate (scope finance:vat): body amount, country, inclusive; returns baseAmount, vatAmount, totalAmount, currency.

KhaleejiAPI does not currently offer a postal address validation endpoint, so this guide does not validate shipping addresses.

1. Shopify: App Proxy backend

Configure an App Proxy in your Shopify app (for example subpath prefix apps, subpath khaleeji) pointing at your backend. Shopify signs each forwarded request with your app secret; verify the signature parameter before calling KhaleejiAPI, and allowlist only the checks you need.

javascript
// server.js: your Shopify app backend (Node.js 20+, Express)
// Shopify App Proxy forwards https://{shop}/apps/khaleeji/* to this server.
import crypto from "node:crypto"
import express from "express"
const app = express()
app.use(express.json())
const KHALEEJI_API_KEY = process.env.KHALEEJI_API_KEY // server-only secret
const SHOPIFY_API_SECRET = process.env.SHOPIFY_API_SECRET
// Verify the App Proxy signature so only Shopify-forwarded requests are accepted.
function isValidProxyRequest(query) {
const { signature, ...rest } = query
if (typeof signature !== "string") return false
const message = Object.keys(rest)
.sort()
.map((key) => `${key}=${[].concat(rest[key]).join(",")}`)
.join("")
const digest = crypto.createHmac("sha256", SHOPIFY_API_SECRET).update(message).digest("hex")
return digest.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature))
}
async function khaleeji(path, params) {
const url = new URL(path, "https://khaleejiapi.dev")
url.search = new URLSearchParams(params).toString()
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KHALEEJI_API_KEY}` },
signal: AbortSignal.timeout(5000),
})
return { status: res.status, body: await res.json() }
}
// Allowlist exactly the checks your storefront needs. Never proxy arbitrary paths.
app.get("/apps/khaleeji/phone", async (req, res) => {
if (!isValidProxyRequest(req.query)) return res.status(401).json({ error: "bad signature" })
const phone = String(req.query.phone ?? "").slice(0, 32)
const { status, body } = await khaleeji("/api/v1/phone/validate", { phone })
res.status(status).json(body)
})
app.get("/apps/khaleeji/vat", async (req, res) => {
if (!isValidProxyRequest(req.query)) return res.status(401).json({ error: "bad signature" })
const trn = String(req.query.trn ?? "").slice(0, 32)
const country = String(req.query.country ?? "AE").slice(0, 2)
const { status, body } = await khaleeji("/api/v1/vat/validate", { trn, country })
res.status(status).json(body)
})
app.listen(3000)

Storefront code then calls your shop's own proxy path:

javascript
// Theme or storefront JavaScript: calls YOUR shop's App Proxy path.
// No KhaleejiAPI key is present in browser code.
// Returns "valid", "invalid", or "unavailable"; only "invalid" should block the shopper.
async function checkPhone(phone) {
try {
const res = await fetch(`/apps/khaleeji/phone?phone=${encodeURIComponent(phone)}`)
if (!res.ok) return { status: "unavailable" } // 4xx/5xx/429: fail open
const { data } = await res.json()
if (data?.valid === true) return { status: "valid", e164: data.formats.e164 }
if (data?.valid === false) return { status: "invalid" }
return { status: "unavailable" }
} catch {
return { status: "unavailable" } // network or JSON errors: fail open
}
}

Scope: theme JavaScript runs on storefront and cart pages, not in Shopify's hosted checkout, so this is an advisory pre-checkout check. Enforcing validation at checkout requires a separate, Shopify-supported checkout integration (see Shopify checkout apps), which this guide does not cover.

Add your own per-session rate limiting on the proxy. Signature verification proves a request came through Shopify, not that the shopper is trusted.

2. WooCommerce: server-side classic checkout hook

Prerequisite: this applies to the classic (shortcode) checkout. The woocommerce_after_checkout_validation hook does not run for the Checkout Block, which submits through the Store API; block checkout needs a separate server-side Store API integration (see WooCommerce's hook alternatives reference).

WooCommerce validates classic checkout in PHP, so the request already runs on your server. Store the key as a constant in wp-config.php (or an environment variable) and validate the billing phone in woocommerce_after_checkout_validation.

php
<?php
// wp-config.php (server-only, never in theme JavaScript):
// define( 'KHALEEJI_API_KEY', getenv( 'KHALEEJI_API_KEY' ) );
add_action( 'woocommerce_after_checkout_validation', function ( $data, $errors ) {
if ( ! defined( 'KHALEEJI_API_KEY' ) || empty( $data['billing_phone'] ) ) {
return;
}
$url = add_query_arg(
array( 'phone' => rawurlencode( $data['billing_phone'] ) ),
'https://khaleejiapi.dev/api/v1/phone/validate'
);
$response = wp_remote_get( $url, array(
'timeout' => 5,
'headers' => array( 'Authorization' => 'Bearer ' . KHALEEJI_API_KEY ),
) );
// Fail open on network errors so an outage never blocks checkout.
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( isset( $body['data']['valid'] ) && false === $body['data']['valid'] ) {
$errors->add(
'validation',
__( 'Please enter a valid GCC phone number including the country code, e.g. +971 50 123 4567.', 'your-textdomain' )
);
}
}, 10, 2 );

3. Preview GCC VAT on the server

Use /api/v1/vat/calculate to show or cross-check VAT for a GCC country. Keep your platform's own tax settings (Shopify Taxes, WooCommerce tax rates) as the source of truth for charged tax, and confirm rates and exemptions with your tax adviser.

bash
curl -X POST "https://khaleejiapi.dev/api/v1/vat/calculate" \
-H "Authorization: Bearer $KHALEEJI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "amount": 250, "country": "SA", "inclusive": false }'
# Response (data object)
# { "country": "SA", "countryName": "Saudi Arabia", "vatRate": 15,
# "inputAmount": 250, "inclusive": false, "baseAmount": 250,
# "vatAmount": 37.5, "totalAmount": 287.5, "currency": "SAR", ... }

Production checklist

  • The API key exists only in server environment variables or wp-config.php.
  • Proxies allowlist specific endpoints and cap input length; they never forward arbitrary paths.
  • Timeouts are short and validation fails open, so a KhaleejiAPI outage never blocks a sale.
  • Keys use the minimum scopes: validation:phone, validation:vat, finance:vat.