@morapay/sdk
v0.1.0
Published
Morapay merchant integration SDK (payment links, open checkout, webhooks)
Readme
@morapay/sdk
Merchant integration SDK for Morapay — payment links, products, quotes, webhooks, and browser checkout helpers.
Version: 0.1.0 (first public release)
| Package | Install |
|---------|---------|
| @morapay/sdk | npm install @morapay/sdk |
| @morapay/sdk-nextjs | npm install @morapay/sdk-nextjs (Next.js BFF helpers) |
| create-morapay-app | npx create-morapay-app |
See CHANGELOG.md.
TypeScript client for the Morapay Merchant API v1.
Install
npm install @morapay/sdkpnpm add @morapay/sdkyarn add @morapay/sdkbun add @morapay/sdkScaffold an app
npx create-morapay-app
# or: pnpm dlx | yarn dlx | bunx create-morapay-app| Template | Stack | Checkout |
|----------|-------|----------|
| Next.js (fiat) | App Router + API routes | Widget modal or redirect |
| Next.js (crypto) | + Dynamic/wagmi wallet bridge | Widget + on-site wallet |
| React | Vite + Express API | Widget modal or redirect (fiat — no Dynamic) |
| Express | API only | Your frontend calls the API |
| Python | FastAPI API only | venv + pip — no Node deps in generated app |
Prompts: pick a template; Next.js also asks whether customers connect a crypto wallet on your site.
Run the scaffolder with any of: npx · pnpm dlx · yarn dlx · bunx create-morapay-app.
@morapay/sdk has no wallet dependencies — wallets ship only in the Next.js crypto template.
Crypto payments — when do you need Dynamic?
| What you're building | NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID? |
|----------------------|---------------------------------------|
| Fiat only (MoMo, card, bank) — any template | No |
| Redirect customers to checkout.morapay.io for crypto | No — wallet UI runs on Morapay checkout |
| Widget live mode (iframe to hosted checkout) | No — same hosted checkout handles crypto |
| Widget preview with crypto quotes + connect wallet on your site | Yes — Next.js crypto template only |
Dynamic is a merchant-site wallet bridge for preview UX. Real crypto charges still settle through Morapay checkout; you do not need a Dynamic project just to accept crypto payments via redirect or live widget iframe.
Python backends
No Python package on PyPI yet. Scaffold a FastAPI backend sample with create-morapay-app (option 4), or browse the reference app:
https://github.com/morapay-app/sdk-examples/tree/main/python-api
After scaffolding, use Python only (venv + pip) — npm/pnpm/yarn/bun are only for running the scaffolder.
Quick start
Server-side only. Never embed sk_* in browsers, mobile apps, or checkout bundles.
Only API keys are required. baseUrl and checkoutBaseUrl default to production Morapay hosts.
import { Morapay } from "@morapay/sdk";
const morapay = new Morapay({
publicKey: process.env.MORAPAY_PUBLIC_KEY!,
secretKey: process.env.MORAPAY_SECRET_KEY!,
});
const qr = await morapay.qr.get();
console.log(qr.checkoutUrl, qr.qrPayload);
const { data: links } = await morapay.link();
const link = await morapay.link.create({
title: "Invoice #42",
amount: 100,
currency: "USD",
});Optional overrides when not using Morapay production:
const morapay = new Morapay({
publicKey: process.env.MORAPAY_PUBLIC_KEY!,
secretKey: process.env.MORAPAY_SECRET_KEY!,
baseUrl: process.env.MORAPAY_BASE_URL, // default: https://api.morapay.io
checkoutBaseUrl: process.env.MORAPAY_CHECKOUT_BASE_URL, // default: https://checkout.morapay.io
});Template apps read the same env vars server-side — omit them to hit production; set them explicitly for a local Core/checkout stack.
Callable resources
List and get share one call signature — pass a query object (or nothing) to list, pass an ID string to fetch one:
const { data: products } = await morapay.products({ limit: 10 });
const product = await morapay.products("prod_123");
const { data: links } = await morapay.link();
const link = await morapay.link("link_abc");
const { data: batches } = await morapay.disbursement();
const batch = await morapay.disbursement("batch_xyz");
const { data: subs } = await morapay.subscriptions();
const sub = await morapay.subscriptions("sub_456");
const { data: plans } = await morapay.subscriptions.plans();
const plan = await morapay.subscriptions.plans("plan_789");Create, update, and scoped helpers attach to the same function object:
await morapay.products.create({ name: "API Access", price: 500 });
await morapay.products.ensureCheckoutLink("prod_123"); // catalog — reuse or create once
await morapay.products.link("prod_123", { isOneTime: true }); // invoice — always new
await morapay.products.links("prod_123");
await morapay.link.sessions("link_abc");
await morapay.link.quote("link_abc", { chainId: 8453 });
await morapay.disbursement.account();
await morapay.disbursement.quote({ chainId: 8453, token: "USDC", payoutTotal: 250, recipientCount: 10 });
await morapay.disbursement.ledger();Authentication
Every request is signed with HMAC-SHA256:
| Header | Value |
|--------|--------|
| Morapay-Key | Public key (pk_live_* or pk_test_*) |
| Morapay-Timestamp | Unix seconds |
| Morapay-Signature | v1=<hex> |
The SDK builds the canonical payload and signs automatically. TEST/LIVE is determined by the key pair — do not send x-merchant-environment with API keys.
Create key pairs in the dashboard under Developers → API keys. Each key is pinned to TEST or LIVE at creation time.
Resources
| SDK property | API path |
|--------------|-----------|
| morapay.link() / .create / .update / .sessions / .quote | /links |
| morapay.products() / .create / .update / .link / .links | /products, /links |
| morapay.disbursement() / .account / .quote / .ledger / .cancel | /disbursement/* |
| morapay.qr | /qr |
| morapay.quotes.fiat | /quotes/fiat |
| morapay.quotes.ramp | /quotes/ramp |
| morapay.subscriptions() / .plans / .create / .pause / .cancel | /subscriptions |
| morapay.webhooks.endpoints() / .create / .update / .delete / .revealSecret | /webhooks/endpoints |
| morapay.utils | local helpers (no network) |
Products and payment tracking
Morapay links fall into two integration patterns:
| Pattern | SDK call | When |
|---------|----------|------|
| Catalog / product checkout | products.ensureCheckoutLink(productId) | Ecommerce, SaaS plans, fixed-price SKUs — one reusable link per product |
| Invoice / order checkout | products.link(productId, { isOneTime: true }) | One payment per order — new link per checkout |
isOneTime: false (unlimited) means the same publicCode accepts many payments. isOneTime: true means the link is consumed after one successful payment.
const product = await morapay.products.create({
name: "Annual membership",
price: 49,
currency: "GHS",
type: "DIGITAL",
});
// Ecommerce: reuse existing catalog link or create once
const catalog = await morapay.products.ensureCheckoutLink(product.id);
console.log(morapay.buildCheckoutUrl(catalog.publicCode)); // stable for this SKU
// Invoice: fresh link per order (retries dedupe via orderId / idempotencyKey)
const invoice = await morapay.products.link(product.id, {
isOneTime: true,
customerEmail: "[email protected]",
metadata: { orderId: "ord_123" },
idempotencyKey: "ord_123",
});Store publicCode on your product row (or in Morapay link metadata) so Buy now opens checkout without creating links on every click.
Invoice retries dedupe server-side when you send Morapay-Idempotency-Key (or metadata.orderId / idempotencyKey in the body). The SDK sets the header automatically from products.link({ idempotencyKey }).
console.log(morapay.utils.isPaid(checkout)); // false until payer completes checkout
const { data: links } = await morapay.products.links(product.id);
for (const link of links) {
console.log(link.metadata, link.paidAt, link.paidByWalletAddress);
}Sync utilities
Pure helpers live under morapay.utils — no await, no network:
morapay.utils.isPaid(link);
morapay.utils.verifyWebhook({ payload, signature, secret });Business QR checkout
const qr = await morapay.qr.get();
// qr.checkoutUrl → https://checkout.morapay.io/pay/{slug}
morapay.buildBusinessPayCheckoutUrl("acme-store");
morapay.buildBusinessPayWidgetUrl("acme-store"); // adds ?embed=1Frontends
Two integration patterns — pick one with checkoutMode: "widget" | "redirect". Server SDK calls stay the same.
| Pattern | checkoutMode | When to use |
|---------|----------------|-------------|
| Widget modal | "widget" | Checkout on your page — fiat + crypto via Morapay iframe |
| Hosted redirect | "redirect" | Email/SMS links, simple “Pay now” — opens checkout.morapay.io |
import { openPaymentLinkCheckout } from "@morapay/sdk/widget";
await openPaymentLinkCheckout({
publicCode: link.publicCode,
checkoutMode: "widget", // or "redirect"
widgetExperience: "live", // widget only: "preview" | "live"
checkoutBaseUrl: "https://checkout.morapay.io",
// widgetScriptUrl optional — defaults to Morapay CDN (drop-in, no extra install)
});Install @morapay/sdk once — server API + browser widget helpers (@morapay/sdk/widget). The widget bundle (morapay-checkout.js) loads from Morapay CDN by default, or self-host at /widget/ in your app.
Checkout modals (hybrid architecture)
Morapay uses a hybrid model like RainbowKit / Web3Modal — not a full-page outer iframe:
- Native modal (
<morapay-checkout-modal>, Shadow DOM) on the host page — backdrop, scroll lock, wallet list, access towindow.ethereum - Secure inner iframe (
?hybrid=1&canvas=1) only for card / MoMo / KYC — PCI-sensitive flows stay oncheckout.morapay.io
<script src="https://checkout.morapay.io/widget/morapay-checkout.js"></script>
<script>
MorapayCheckout.openModal({
mode: "payment-link",
publicCode: "abc123",
onSuccess: (payload) => console.log("paid", payload),
});
</script>Server-side URL helpers:
morapay.buildPaymentLinkEmbedUrl("abc123", { flow: "onramp" });Expose a thin server proxy (Next.js route, Cloudflare Worker, etc.) that holds sk_* and forwards signed requests.
Error handling
Every failed API call throws a typed MorapayError subclass with six consultant fields:
| Property | Audience | Purpose |
|----------|----------|---------|
| message | Developer logs | Technical diagnostic (RFC details, field paths) |
| safeMessage | End-customer UI | Safe toast/modal copy — no secrets or internals |
| displayMessage | UI components | Intent-driven display copy (@morapay/react, MorapayUi.toast) |
| uiHint | UI components | { severity, placement, actionText? } rendering blueprint |
| code | Programmatic | Semantic namespace code (links.amount.below_minimum) |
| requestId | Support | Correlate with API logs (Morapay-Request-Id header) |
| docUrl | Self-serve | Direct link to docs.morapay.io/codes/... |
| details | Validation | Field-level Zod or business-rule breakdown |
import {
Morapay,
MorapayInvalidRequestError,
MorapayAuthenticationError,
MorapayErrorCode,
isMorapayError,
} from "@morapay/sdk";
try {
await morapay.products.link("prod_123", { amount: 0.1, currency: "GHS" });
} catch (err) {
if (err instanceof MorapayInvalidRequestError) {
showToast(err.safeMessage); // safe for checkout UI
console.error(err.format()); // consultant layout in terminal
} else if (err instanceof MorapayAuthenticationError) {
alertDevOps(err.requestId);
} else if (isMorapayError(err) && err.code === MorapayErrorCode.PayoutsMomoInsufficientBalance) {
await queuePayoutRetry(err.requestId);
}
}Use MorapayErrorCode for compile-time-safe comparisons — your IDE autocompletes every documented code and catches typos before production.
Browser toast (vanilla JS)
import { MorapayUi, isMorapayError } from "@morapay/sdk";
try {
await morapay.disbursement.quote({ ... });
} catch (err) {
MorapayUi.toast(err, { onAction: () => location.reload() });
}Morapay.ui.toast is also available on the client instance.
Docs & examples
- API reference: https://docs.morapay.io
- Starter apps:
npx create-morapay-app(Next.js, React, Express, or Python) - Python reference: https://github.com/morapay-app/sdk-examples/tree/main/python-api
- Next.js integration helpers: https://github.com/morapay-app/sdk/tree/main/nextjs
License
MIT — see LICENSE.
