@hadawi/sdk

@hadawi/sdk

Official Hadawi SDK — add group contribution checkout to any storefront.

Hadawi lets groups of people split the cost of a purchase. The SDK handles:

  • Server-side: creating checkout intents (a session identifier for one purchase) using your merchant API key.
  • Client-side: official branded checkout buttons plus redirect/popup open helpers.
  • Webhooks: verifying and parsing events sent to your server when a session is funded.

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>

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 navigation
  • popup — 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:

  1. Log in to the merchant portal
  2. Open Developer → Webhook
  3. Enter your HTTPS endpoint (e.g. https://yourstore.com/webhooks/hadawi)
  4. Save, then Reveal the signing secret (whsec_…) and store it as HADAWI_WEBHOOK_SECRET

Webhook 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_fundingfundednotifiedfulfillingdelivered
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";

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