npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@graphland/pgw-merchant

v3.4.0

Published

Merchant/client SDK for Graphland Payment Gateway — create invoices, check status, and verify webhooks

Readme

@graphland/pgw-merchant

A lightweight, zero-dependency SDK for merchants integrating with the Graphland Payment Gateway.

Contents

Features

  • Create invoices — initiate payment requests with idempotency support
  • Get invoices — retrieve full invoice details
  • Get customer invoices — list billing invoices for a customer
  • Check invoice status — quickly see if an invoice is pending, paid, or failed
  • Billing & entitlements — billing customers, subscriptions, add-on purchases, entitlement checks and consumption
  • NestJS module — injectable gateway and entitlement guardrails for server-side billing and checkout
  • React bindings — bootstrap provider, hooks, and FeatureGate for browser feature gating
  • System payment adapters — framework-free bKash and SSLCommerz adapters for owned credentials
  • Verify webhooks — cryptographically verify incoming webhook payloads using HMAC-SHA256

Installation

npm install @graphland/pgw-merchant
# or
yarn add @graphland/pgw-merchant
# or
pnpm add @graphland/pgw-merchant
# or
bun add @graphland/pgw-merchant

Optional peer dependencies:

  • @nestjs/common — for @graphland/pgw-merchant/nest
  • react — for @graphland/pgw-merchant/react

Package subpaths

| Import | Use for | | -------------------------------- | --------------------------------------------------------- | | @graphland/pgw-merchant | Core client, webhooks, types | | @graphland/pgw-merchant/nest | NestJS module, gateway injection, entitlement enforcement | | @graphland/pgw-merchant/react | Browser bootstrap provider, hooks, FeatureGate | | @graphland/pgw-merchant/system | Reusable bKash and SSLCommerz provider protocol adapters |

Quick Start

import {
  GraphlandPGWClient,
  verifyWebhookSignature,
  parseWebhookEvent,
} from "@graphland/pgw-merchant";
import type { WebhookEvent } from "@graphland/pgw-merchant";

// ── Initialise the client ──────────────────────────────────────
const pgw = new GraphlandPGWClient({
  apiKey: "graphland_....", // from the Graphland Console
  clientId: "my-store", // your client slug (lowercased)
});

// ── 1. Create an invoice ───────────────────────────────────────
const invoice = await pgw.createInvoice({
  identifier: "order-123", // your unique payment reference (idempotency)
  amount: 1000, // 1000 = 10.00 in your currency
  currency: "BDT",
  description: "Order #123",
  customer: {
    name: "John Doe",
    email: "[email protected]",
    phone: "+8801700000000",
  },
  successUrl: "https://myshop.com/order/success",
  cancelUrl: "https://myshop.com/order/cancel",
  failUrl: "https://myshop.com/order/fail",
  metadata: {
    orderId: "order-123",
  },
});

console.log(invoice.paymentUrl); // → hosted checkout URL

// ── 2. Check invoice status ────────────────────────────────────
const status = await pgw.getInvoiceStatus(invoice.invoiceUID);
console.log(status.status); // "pending" | "paid" | "failed"

// ── 3. Get full invoice details ────────────────────────────────
const full = await pgw.getInvoice(invoice.invoiceUID);
console.log(full.invoice.amount, full.client.displayName);

// Pay an already-created invoice. clientId comes from the SDK constructor.
const payment = await pgw.payInvoice(invoice.invoiceUID, {
  successUrl: "https://merchant.example.com/payment/success",
  cancelUrl: "https://merchant.example.com/payment/cancel",
  failUrl: "https://merchant.example.com/payment/failed",
});
console.log(payment.paymentUrl); // redirect the payer to this URL

Webhook Verification

Graphland signs every webhook POST with your webhook signing secret (get it from the Console or when you create your client). Always verify the signature before acting on the event.

import {
  verifyWebhookSignature,
  parseWebhookEvent,
} from "@graphland/pgw-merchant";
import type { WebhookEvent } from "@graphland/pgw-merchant";

// Example using Express / Hono / Node http
app.post("/webhooks/graphland", (req, res) => {
  const rawBody = req.rawBody; // must be the exact raw body bytes as a string
  const secret = "your-webhook-signing-secret";

  // 1. Verify HMAC-SHA256 signature (timestamp + raw body)
  if (
    !verifyWebhookSignature(
      rawBody,
      {
        signature: req.headers["x-graphland-signature"],
        timestamp: req.headers["x-graphland-timestamp"],
      },
      secret,
    )
  ) {
    return res.status(401).send("Invalid signature");
  }

  // 2. Parse the event payload
  const event: WebhookEvent = parseWebhookEvent(JSON.parse(rawBody));

  // 3. Handle the event
  switch (event.type) {
    case "payment.paid":
      console.log("Payment received:", event.data);
      break;
    case "payment.failed":
      console.log("Payment failed:", event.data);
      break;
    default:
      console.log("Unhandled event type:", event.type);
  }

  res.status(200).send("OK");
});

API Reference

GraphlandPGWClient

| Method | Description | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | createInvoice(input) | Create or upsert an invoice. Optional items (name, quantity, unitPrice) appear on the invoice document; their total must equal amount. If identifier matches an existing PENDING invoice, the invoice is updated (idempotent). | | getInvoice(invoiceUID) | Retrieve full invoice details including merchant display info. | | getCustomerInvoices(billingCustomerId) | List the customer's billing invoices, newest first. | | getCustomerQuotations(billingCustomerId, { status? }) | List quotations sent to the customer, newest first (drafts excluded). Each has status (sent, accepted, declined, expired), publicUrl for the accept/decline page, and invoiceUID once converted. | | getInvoiceStatus(invoiceUID) | Get the current status (pending, paid, failed) and redirect URLs. | | getPublicPreview(invoiceUID, { payNow? }) | Get { url } of the public, read-only invoice page on hosted checkout. Shows Pay now while unpaid; pass payNow: false to hide it. Redirect to it. | | getInvoiceDocument(invoiceUID) | Render-ready invoice document: seller, billed-to, line items, subtotal, discount, total, amount due. | | payInvoice(invoiceNumber, input) | Return hosted checkout for an existing invoice, optionally refreshing return URLs; creates no invoice. |

verifyWebhookSignature(rawBody, headers, secret)

Returns true if the HMAC-SHA256 signature matches HMAC(secret, "{timestamp}.{rawBody}").

parseWebhookEvent(body)

Validates and parses an incoming webhook JSON body into a typed WebhookEvent object.

V2 — Billing & Entitlements

Manage billing customers, subscriptions, purchases, and entitlements. All *Subscription / *Purchase / ensureCustomer calls take an idempotencyKey that the API deduplicates on.

A runnable end-to-end walkthrough lives in the sdk-playground app — customer, free & paid subscriptions (paid returns a paymentUrl and activates after settlement), plan upgrades and downgrades, consume/check with idempotent replays, and cancellation.

const pgw = new GraphlandPGWClient({ apiKey, clientId: "waland" });

// 1. Create or update a billing customer
await pgw.ensureCustomer(
  "org_acme",
  { displayMetadata: { tier: "gold" } },
  "req-1",
);

// 2. Start a subscription for a plan + price variant
//    Free variants activate instantly; paid variants return
//    status "pending_payment" + paymentUrl until the invoice settles.
const sub = await pgw.createSubscription(
  { billingCustomerId: "org_acme", planKey: "pro", priceVariantKey: "30d" },
  "req-2",
);
console.log(sub.status); // "active" (free) | "pending_payment" (paid)

const checkout = await pgw.createCheckoutSession(
  "org_acme",
  {
    planKey: "pro",
    priceVariantKey: "30d",
    successUrl: "https://merchant.example.com/billing/success",
    cancelUrl: "https://merchant.example.com/billing/cancel",
    failUrl: "https://merchant.example.com/billing/failed",
  },
  "req-checkout",
);

// 3. Upgrade or downgrade — direction is derived from selected variant prices
const change = await pgw.changePlan(
  sub.id,
  { planKey: "enterprise", priceVariantKey: "30d" },
  "req-3",
);
// Upgrades charge the prorated difference and apply on settlement;
// downgrades schedule at period end and invoice the new variant then.
if (change.status === "pending_payment") {
  console.log(change.invoiceAmount, change.paymentUrl); // charged now
}
// `change.id` is a new subscription that supersedes `sub.id` — use it from here.

// 4. Schedule cancellation at period end
await pgw.cancelSubscription(change.id, "req-4");

// 5. Create an invoice-backed add-on purchase
const purchase = await pgw.createPurchase(
  {
    billingCustomerId: "org_acme",
    addonKey: "token_pack_10k",
    successUrl: "https://waland.app/success",
  },
  "req-5",
);
console.log(purchase.paymentUrl); // hosted checkout for the add-on

// 6. Read / explain an entitlement
const ent = await pgw.getEntitlement("org_acme", "analytics");
const why = await pgw.explainEntitlement("org_acme", "analytics");

// 7. Check a non-consumable quantity (denial is NOT thrown — it's a 200 response)
const check = await pgw.check({
  billingCustomerId: "org_acme",
  featureKey: "projects",
  requestedQuantity: 5,
});
if (!check.allowed) console.log("Denied:", check.code); // "ENTITLEMENT_EXCEEDED"

// 8. Consume a consumable quantity (idempotent under requestId)
const consumed = await pgw.consume({
  billingCustomerId: "org_acme",
  featureKey: "ai_tokens",
  quantity: 500,
  requestId: "req_01HX7QK9F2V8B6M3C1L4N5P6Q",
});

NestJS

Import @graphland/pgw-merchant/nest to register a global billing module with a private-key PgwGateway and EntitlementService. Full guide: src/nest/README.md.

import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { PGWBillingModule } from "@graphland/pgw-merchant/nest";

@Module({
  imports: [
    ConfigModule.forRoot(),
    PGWBillingModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        apiKey: config.getOrThrow("PGW_API_KEY"),
        clientId: config.getOrThrow("PGW_CLIENT_ID"),
        baseUrl: config.get("PGW_BASE_URL"),
        consoleUrl: config.get("APP_URL", "http://localhost:5173"),
        displayName: "My Store",
        defaultPlanKey: "free",
        defaultPriceVariantKey: "30d",
      }),
    }),
  ],
})
export class AppModule {}

forRoot(config) is available for static configuration. When apiKey or clientId is missing, the gateway logs a warning and throws on use.

Checkout and bootstrap

Inject PAYMENT_GATEWAY (default: PgwGateway) for server-side billing:

import { Controller, Inject, Post, Body } from "@nestjs/common";
import {
  PAYMENT_GATEWAY,
  type PaymentGateway,
} from "@graphland/pgw-merchant/nest";

@Controller("billing")
export class BillingController {
  constructor(
    @Inject(PAYMENT_GATEWAY) private readonly gateway: PaymentGateway,
  ) {}

  @Post("checkout")
  async checkout(
    @Body()
    body: {
      organizationId: string;
      planKey: string;
      priceVariantKey: string;
      customerEmail?: string;
    },
  ) {
    const session = await this.gateway.createCheckoutSession(body);
    return { redirectUrl: session.redirectUrl, invoiceId: session.invoiceId };
  }
}

createCheckoutSession sends one request to PGW. The API owns customer provisioning, trial eligibility, and create-vs-change subscription decisions.

| Method | Description | | ----------------------------------------------- | -------------------------------------------------- | | ensureBillingAccount(billingCustomerId) | Create customer + baseline subscription if missing | | getCatalog() | Published plans, variants, add-ons, features | | getBootstrap(billingCustomerId) | Catalog + subscription + entitlements | | getEntitlement(billingCustomerId, featureKey) | Effective entitlement | | check(input) | Non-consumable limit check | | consume(input) | Consumable usage under requestId | | createCheckoutSession(input) | Subscribe or change plan with hosted checkout |

Entitlement enforcement

Inject EntitlementService in request handlers. assertAndConsume throws ForbiddenException when access is denied:

import { Controller, Post, Body } from "@nestjs/common";
import { EntitlementService } from "@graphland/pgw-merchant/nest";

@Controller("ai")
export class AiController {
  constructor(private readonly entitlements: EntitlementService) {}

  @Post("generate")
  async generate(@Body() body: { tenant: string; currentProjects: number }) {
    await this.entitlements.assertAndConsume({
      tenant: body.tenant,
      featureKey: "ai_tokens",
      currentQuantity: body.currentProjects,
    });
    return { ok: true };
  }
}

Use @graphland/pgw-merchant/react for browser UI gating; use EntitlementService on the server for enforcement.

React

For browser feature gating, create a public API key and use the React subpath. Full guide: src/react/README.md.

The provider makes one bootstrap request and keeps the result in memory; FeatureGate and the hooks below read that stored snapshot and do not make additional requests. The bootstrap response also includes the published catalog, so browser billing UI can render plan and feature metadata without calling the private catalog endpoint. When payment is due, it includes subscription.graceEndsAt and pendingInvoice with the invoice number and hosted paymentUrl, which can also be refreshed with payInvoice.

import {
  FeatureGate,
  PgWProvider,
  usePgWContext,
  useEntitlement,
} from "@graphland/pgw-merchant/react";

function TokenBalance() {
  const { entitlement } = useEntitlement("ai_tokens");
  return <span>{entitlement?.remaining ?? 0} tokens remaining</span>;
}

function Plans() {
  const { data } = usePgWContext();
  const catalog = data?.catalog;
  return <span>{catalog?.plans.length ?? 0} plans available</span>;
}

export function App() {
  return (
    <PgWProvider
      clientId="my-store"
      billingCustomerId="org_acme"
      publicApiKey="graphland_..."
      baseUrl="https://pgw.graphland.dev"
    >
      <FeatureGate
        featureKey="analytics"
        loadingFallback={<span>Loading access…</span>}
        fallback={<span>Upgrade to unlock analytics.</span>}
      >
        <Analytics />
      </FeatureGate>
      <TokenBalance />
    </PgWProvider>
  );
}

The public key is intentionally visible in browser code and can call the published catalog and customer bootstrap endpoints. The bootstrap response contains the published catalog and the computed entitlement snapshot. FeatureGate is not security enforcement; protected operations must still be authorized by the merchant server with a private key.

To show a customer's billing history in the browser, use the invoice hook:

import {
  PgWProvider,
  useCustomerInvoices,
} from "@graphland/pgw-merchant/react";

function InvoiceHistory() {
  const { data: invoices, loading, error } = useCustomerInvoices();
  if (loading) return <span>Loading invoices…</span>;
  if (error) return <span>Unable to load invoices.</span>;
  return <span>{invoices?.length ?? 0} invoices</span>;
}

Billing API Reference

| Method | Description | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ensureCustomer(billingCustomerId, input, idempotencyKey) | Create or update a billing customer (PUT). | | createSubscription(input, idempotencyKey) | Start a free active subscription. | | getSubscription(billingCustomerId) | Read the customer's current base subscription. | | getCatalog() | Read the published plans, prices, add-ons, and feature definitions. Public or private key. | | getBootstrap(billingCustomerId) | Read the published catalog, current subscription, and all effective entitlements in one request. | | changePlan(id, input, idempotencyKey) | Upgrade or downgrade; returns the superseding subscription. | | cancelPlanChange(id, idempotencyKey) | Discard a plan change that has not applied yet. | | cancelSubscription(id, idempotencyKey) | Schedule cancellation at period end. | | createPurchase(input, idempotencyKey) | Create an invoice-backed add-on purchase. | | getEntitlement(billingCustomerId, featureKey) | Read the effective entitlement. | | explainEntitlement(billingCustomerId, featureKey) | Explain an entitlement decision by grant. | | check(input) | Check a non-consumable quantity; denial returns allowed: false (HTTP 200). | | consume(input) | Consume a consumable quantity under a caller requestId. |

System payment adapters

@graphland/pgw-merchant/system exposes the provider protocol layer used by Graphland itself. It has no NestJS, database, tenant, invoice, or Graphland API dependency. Applications own credential storage and webhook routes, then pass the active store's plain configuration into each operation.

import {
  BkashPaymentAdapter,
  SslcommerzPaymentAdapter,
} from "@graphland/pgw-merchant/system";

const bkash = new BkashPaymentAdapter();
const sslcommerz = new SslcommerzPaymentAdapter();

const intent = await sslcommerz.createPaymentIntent(
  {
    transactionId: order.id,
    amount: order.total,
    currency: "BDT",
    customer: { name: order.customerName, phone: order.customerPhone },
    successUrl: `${storefrontUrl}/orders/${order.id}/success`,
    failUrl: `${storefrontUrl}/orders/${order.id}/failed`,
    cancelUrl: `${storefrontUrl}/orders/${order.id}/cancelled`,
    ipnUrl: `${storefrontApiUrl}/v1/webhooks/sslcommerz`,
  },
  store.paymentConfig.sslcommerz,
);

Adapter instances are reusable across tenants because credentials are never stored on the instance. bKash access tokens are cached by provider base URL and app key. Use handleCallback for bKash browser callbacks and handleIpn for SSLCommerz IPNs; the application remains responsible for locating the order, checking amount/currency against it, applying settlement idempotently, and returning the HTTP response.

The bKash adapter implements URL Checkout mode 0011. A successful browser callback is finalized through /tokenized/checkout/execute; if that single-use request has an uncertain response, the adapter recovers through /tokenized/checkout/payment/status. Failure and cancel browser callbacks are returned as ignored navigation events and never treated as authoritative settlement. All bKash requests use a 30-second timeout by default (override with new BkashPaymentAdapter({ timeoutMs })). queryPayment(config, paymentID) is also available for explicit reconciliation. If you verify browser callback signatures, persist the opaque signature values from the callback URLs in the Create Payment raw response and compare them with verifyBkashCallbackSignature; do not attempt to recreate them with HMAC.

Error Handling

The SDK throws typed errors that include the HTTP status code and the API's structured error code (VALIDATION_ERROR, IDEMPOTENCY_CONFLICT, ENTITLEMENT_EXCEEDED, …) when present:

import { ApiError, ConflictApiError, ValidationApiError, EntitlementExceededApiError } from '@graphland/pgw-merchant';

try {
  await pgw.createInvoice({ ... });
} catch (err) {
  if (err instanceof ValidationApiError) {
    console.error('Validation:', err.code, err.message);
  } else if (err instanceof ConflictApiError) {
    console.error('Conflict:', err.code, err.message);
  } else if (err instanceof EntitlementExceededApiError) {
    console.error('Entitlement exceeded:', err.code);
  } else if (err instanceof ApiError) {
    console.error(`API error ${err.statusCode}:`, err.body);
  } else {
    console.error('Network error:', err);
  }
}

Retries

Set retries to retry transient failures (network errors, 408/429/5xx) with exponential backoff. Retries only happen for requests that are safe to re-send: GETs, and writes carrying an idempotency-key, a consume requestId, or an invoice identifier — a non-idempotent POST is never retried.

const pgw = new GraphlandPGWClient({
  apiKey,
  clientId: "waland",
  retries: 3,
  retryDelayMs: 200,
});

Configuration

| Option | Default | Description | | -------------- | --------------------------- | --------------------------------------------- | | apiKey | — | Your Graphland integration API key (required) | | clientId | — | Your lowercased client slug (required) | | baseUrl | https://pgw.graphland.dev | Override the API base URL for testing | | timeout | 15_000 | Request timeout in milliseconds | | retries | 0 | Retry count for idempotent requests | | retryDelayMs | 200 | Base backoff between retries |

Publishing (maintainers)

From packages/merchant-sdk:

npm run build
npm pack --dry-run   # inspect tarball contents
npm login            # npmjs.com account in @graphland org
npm publish --access public

Scoped packages require --access public on first publish (also set in publishConfig). Bump version in package.json for each release.

License

MIT