@hadawi/sdkOfficial Hadawi SDK — add group contribution checkout to any storefront.
Hadawi lets groups of people split the cost of a purchase. The SDK handles:
| Surface | Package | Runs on |
|---|---|---|
| Server (intents, orders, webhooks) | @hadawi/sdk |
Node.js ≥ 18 |
| Browser (branded button + open checkout) | @hadawi/sdk/browser |
Any modern browser |
Supported storefronts: plain HTML, React, Next.js, Vue, Svelte, Angular, and similar web frameworks. There is no separate React/Vue package — mount into a DOM node with mountButton().
Not supported yet: native iOS/Android, React Native, or Flutter SDKs. Checkout is the hosted web flow (popup or redirect).
npm install @hadawi/sdk
# or
pnpm add @hadawi/sdk
# or
yarn add @hadawi/sdk
# or
bun add @hadawi/sdk
Use the SDK button — do not invent your own “Pay / Split” CTA. Official variants keep Hadawi branding consistent on every storefront. On click, call your server (step 2) for an intentId.
Import from @hadawi/sdk/browser, mount into a ref, and call destroy() on unmount:
"use client"; // Next.js App Router only
import { useEffect, useRef } from "react";
import { HadawiCheckout } from "@hadawi/sdk/browser";
export function HadawiPayButton({ productId }: { productId: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
const checkout = new HadawiCheckout({ mode: "live" }); // or "dev"
const button = checkout.mountButton({
container: ref.current,
variant: "primary",
label: "split",
getIntentId: async () => {
const res = await fetch("/api/create-contribution", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId }),
});
const data = await res.json();
return data.intentId;
},
open: {
mode: "popup",
onSuccess: () => {
window.location.href = "/thank-you";
},
},
});
return () => button.destroy();
}, [productId]);
return <div ref={ref} />;
}
<script setup>
import { onMounted, onBeforeUnmount, ref } from "vue";
import { HadawiCheckout } from "@hadawi/sdk/browser";
const el = ref(null);
let button;
onMounted(() => {
const checkout = new HadawiCheckout({ mode: "live" });
button = checkout.mountButton({
container: el.value,
variant: "primary",
label: "split",
getIntentId: async () => {
const res = await fetch("/api/create-contribution", { method: "POST" });
const data = await res.json();
return data.intentId;
},
open: {
mode: "popup",
onSuccess: () => {
location.href = "/thank-you";
},
},
});
});
onBeforeUnmount(() => button?.destroy());
</script>
<template>
<div ref="el" />
</template>
<div id="hadawi-checkout"></div>
<script src="https://cdn.jsdelivr.net/npm/@hadawi/sdk/dist/browser.iife.js"></script>
<script>
const checkout = new Hadawi.HadawiCheckout({ mode: "live" });
checkout.mountButton({
container: "#hadawi-checkout",
variant: "primary",
label: "split",
getIntentId: async () => {
const { intentId } = await fetch("/api/create-contribution", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId: "watch-001" }),
}).then((r) => r.json());
return intentId;
},
open: {
mode: "popup",
onSuccess: () => (location.href = "/thank-you"),
},
});
</script>
open() (advanced)If you already have an intentId and need full control, you can still call checkout.open(intentId, { mode: 'popup' | 'redirect', … }). Prefer mountButton() for the shopper-facing CTA.
Power the button’s getIntentId call with your API key from the merchant dashboard.
import { HadawiClient } from "@hadawi/sdk";
const hadawi = new HadawiClient({
apiKey: process.env.HADAWI_API_KEY!, // from Hadawi merchant dashboard
mode: "live", // or "dev" — SDK resolves the API host
webhookSecret: process.env.HADAWI_WEBHOOK_SECRET, // per-merchant secret (Developer → Webhook)
});
// e.g. POST /api/create-contribution
const { intent, redirectUrl } = await hadawi.intents.create({
itemSnapshot: [
{
name: "Luxury Watch Gift Set",
nameAr: "طقم ساعة فاخرة",
price: 150000, // amount in halalas (150,000 = 1,500.00 SAR)
image: "https://example.com/watch.jpg",
reference: "watch-001", // your internal SKU
},
],
totalAmount: 150000, // must equal sum of itemSnapshot[].price
currency: "SAR",
metadata: { orderId: "ord_abc123" }, // echoed back in webhook events
});
// Return intentId to the browser button
res.json({ intentId: intent.id, redirectUrl });
Note on amounts: All monetary values are in the currency's smallest unit. For SAR, that is halalas (100 halalas = 1 SAR).
variant |
Look |
|---|---|
primary |
Purple gradient fill (default) |
dark |
Solid deep purple |
light |
White fill, purple text |
outline |
Transparent with purple border |
label |
English text |
|---|---|
split |
Split with Friends (default) |
split_cart |
Split Cart with Friends |
pay |
Pay with Hadawi |
| (string) | Your custom text |
locale: 'ar' switches copy to Arabic and sets dir="rtl".
| Mode | Behaviour | Best for |
|---|---|---|
redirect |
Navigates the current tab to the hosted checkout | Simple integrations |
popup |
Opens a new browser window (~520×700 px) | SPAs that want to keep page state |
checkout.open() returns a close() function — call it to dismiss a popup programmatically:
const close = checkout.open(intentId, { mode: "popup", onCancel: () => {} });
// …later:
close();
Hadawi’s payment step uses ClickPay’s hosted payment page. That page responds with X-Frame-Options: deny, so browsers refuse to render it inside any <iframe> (including nested frames).
If checkout were embedded in a merchant-site iframe, the user would hit a blank/blocked frame at pay time. For that reason the SDK only supports:
redirect — full top-level navigationpopup — a separate top-level window (same-origin browsing context for Hadawi, then top-level navigation to ClickPay)Do not wrap /checkout/{intentId} in your own iframe either — payment will fail for the same reason.
Hadawi POSTs a signed JSON event to your webhookUrl (set in the merchant dashboard) whenever a session changes state.
Each merchant has a unique signing secret (whsec_…) issued by Hadawi. Copy it from Developer → Webhook → Reveal into HADAWI_WEBHOOK_SECRET. Merchants cannot choose this value — only reveal or rotate it.
Set the webhook URL in the Hadawi merchant dashboard:
https://yourstore.com/webhooks/hadawi)whsec_…) and store it as HADAWI_WEBHOOK_SECRETWebhook URL configuration is dashboard-only — it is not exposed through the SDK.
import { HadawiClient } from "@hadawi/sdk";
import express from "express";
const hadawi = new HadawiClient({
apiKey: process.env.HADAWI_API_KEY!,
mode: "live",
webhookSecret: process.env.HADAWI_WEBHOOK_SECRET!,
});
const app = express();
// ⚠️ Use raw body parser — do NOT parse JSON before this route
app.post(
"/webhooks/hadawi",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.headers["x-hadawi-signature"] as string;
let event;
try {
event = hadawi.webhooks.constructEvent(req.body, signature);
} catch (err) {
console.error("Webhook signature mismatch:", err);
return res.status(400).send("Invalid signature");
}
switch (event.type) {
case "order.funded":
await hadawi.orders.fulfill(event.payload.order_id!);
console.log(
"Order funded, fulfillment started:",
event.payload.order_id,
);
break;
case "order.partially_funded":
console.log("Partial funding — awaiting organiser decision");
break;
case "order.cancelled":
case "order.expired":
console.log("Session ended:", event.type, event.payload.session_id);
break;
}
res.sendStatus(200);
},
);
interface HadawiWebhookEvent {
type:
| "order.funded"
| "order.partially_funded"
| "order.cancelled"
| "order.expired";
payload: {
event: string;
order_id?: string; // present when an Order record exists
session_id: string;
merchant_id: string;
items: ItemSnapshot[];
total_amount: number; // halalas
currency: string;
funded_at: string; // ISO 8601
};
}
The X-Hadawi-Signature header is an HMAC-SHA256 hex digest of the raw JSON body, keyed with your per-merchant HADAWI_WEBHOOK_SECRET (not a global platform secret).
Once a session is funded and an order exists, use hadawi.orders to manage fulfillment:
// List all orders
const { orders } = await hadawi.orders.list();
// Get detail for a single order (includes contribution breakdown)
const { order, contributions } = await hadawi.orders.get(orderId);
// Mark as being fulfilled (you've started processing)
await hadawi.orders.fulfill(orderId);
// Mark as delivered (item shipped / handed over)
await hadawi.orders.deliver(orderId);
pending_funding → funded → notified → fulfilling → delivered
↘ cancelled
The SDK is written in TypeScript and ships full declaration files. All types are exported from @hadawi/sdk (server) and @hadawi/sdk/browser (client).
import type {
HadawiClientConfig,
CreateIntentParams,
CreateIntentResponse,
Order,
OrderDetail,
HadawiWebhookEvent,
WebhookEventType,
} from "@hadawi/sdk";
import type {
HadawiCheckoutConfig,
CheckoutOpenOptions,
CheckoutMode,
} from "@hadawi/sdk/browser";
mode)Merchants only set mode — URLs are resolved inside the SDK:
mode |
Checkout (web) | API |
|---|---|---|
dev |
https://dev.hadawi.sa |
https://dev.hadawi.sa/api |
live |
https://beta.hadawi.sa |
https://beta.hadawi.sa/api |
Checkout opens {host}/checkout/{intentId}.
// Production
new HadawiClient({ apiKey, mode: "live", webhookSecret });
new HadawiCheckout({ mode: "live" });
// Staging / sandbox
new HadawiClient({ apiKey, mode: "dev", webhookSecret });
new HadawiCheckout({ mode: "dev" });
Set these environment variables (.env):
HADAWI_API_KEY=mk_test_... # from seed output / merchant dashboard
HADAWI_WEBHOOK_SECRET=change-me-in-production
Override hosts only when running against a local stack (baseUrl wins over mode):
// Server
const hadawi = new HadawiClient({
apiKey: process.env.HADAWI_API_KEY!,
baseUrl: "http://localhost:4000",
});
// Browser
const checkout = new HadawiCheckout({ baseUrl: "http://localhost:3000" });
The demo-store/ app (sibling of this monorepo, under infra/) is a complete
working example. See demo-store/server.js for the server-side intent
creation and demo-store/public/index.html for the browser-side checkout
trigger. It depends on this SDK via file:../hadawi/packages/sdk, so run
pnpm sdk:build here before starting it.
API reference page: /docs/sdk/api (embeds TypeDoc from /sdk-api/).
Merchant-facing quick start: /docs/sdk.
cd packages/sdk
pnpm run docs # local preview → packages/sdk/docs/
pnpm run docs:web # sync into packages/web/public/sdk-api/ for deploy
See PUBLISHING.md for the release checklist.
Releases are published to npm as @hadawi/sdk via GitHub Actions when you push
a matching tag:
# after bumping version + CHANGELOG
git tag sdk-v0.1.0
git push origin sdk-v0.1.0
Full checklist (secrets, provenance, dry-run): PUBLISHING.md.
MIT