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

@smarthivelabs-devs/payments

v1.2.0

Published

Official Node.js SDK for SmartHive Payments — multi-provider payment processing

Downloads

488

Readme

@smarthivelabs-devs/payments

Official Node.js SDK for SmartHive Payments — multi-provider payment processing for Africa and beyond (Paystack, Hubtel, Stripe).

Installation

npm install @smarthivelabs-devs/payments

Requires Node.js 18+

Quick Start

import { SmartHivePayments } from '@smarthivelabs-devs/payments';

const smarthive = new SmartHivePayments({
  apiKey: process.env.SMARTHIVE_PAYMENTS_API_KEY!,
  baseUrl: process.env.SMARTHIVE_PAYMENTS_BASE_URL!,
  mode: 'sandbox', // default mode; override per-call with mode: 'live'
});

Environment Variables

| Variable | Required | Description | |---|---|---| | SMARTHIVE_PAYMENTS_API_KEY | Yes | Your API key (sk_test_... or sk_live_...) | | SMARTHIVE_PAYMENTS_BASE_URL | Yes | SmartHive Payments API base URL | | SMARTHIVE_PAYMENTS_WEBHOOK_SECRET | Yes (webhooks) | Your webhook secret (whsec_...) |

Sandbox vs Live Mode

Pass mode on each call — or set a default in the constructor and override per-call.

| Mode | Value | Use | |---|---|---| | Sandbox | 'sandbox' | Testing. No real money moves. | | Live | 'live' | Production. Real transactions. |

// Use sandbox globally, override to live for specific calls
const smarthive = new SmartHivePayments({ apiKey: '...', baseUrl: '...', mode: 'sandbox' });

// This call uses the default (sandbox)
const payment = await smarthive.payments.initialize({ amount: '5000', currency: 'GHS', ... });

// Override to live
const livePayment = await smarthive.payments.initialize({ ..., mode: 'live' });

API Key Prefix & Database Routing

SmartHive uses the API key prefix to route every request to the correct database. This is enforced server-side — it cannot be bypassed by the client.

| Key prefix | Routes to | Required mode | |---|---|---| | sk_test_... | Sandbox database | 'sandbox' | | sk_live_... | Live database | 'live' |

The mode on each call must match the key prefix. Mixing them returns 400 Bad Request:

// ✓ Correct — key prefix and mode agree
const smarthive = new SmartHivePayments({ apiKey: 'sk_test_abc123', baseUrl: '...' });
await smarthive.payments.initialize({ amount: '5000', currency: 'GHS', mode: 'sandbox' });

// ✗ Error 400 — key is sk_test_ but mode says live
await smarthive.payments.initialize({ amount: '5000', currency: 'GHS', mode: 'live' });

Recommended pattern — derive mode from the key so they can never mismatch:

const apiKey = process.env.SMARTHIVE_PAYMENTS_API_KEY!;
const mode = apiKey.startsWith('sk_live_') ? 'live' : 'sandbox';

const smarthive = new SmartHivePayments({ apiKey, baseUrl: process.env.SMARTHIVE_PAYMENTS_BASE_URL!, mode });

All transactions, subscriptions, webhooks, and idempotency records are fully isolated between sandbox and live databases. There is no data overlap.


Which Flow to Use

SmartHive Payments has three independent payment entry points. Pick one per payment — never combine them.

| Flow | Entry Point | Transaction created | When to use | |---|---|---|---| | Hosted Checkout (recommended) | checkout.createSession() | Only when the customer pays on the hosted page | You want SmartHive to handle the UI — method selection, card form, provider redirects | | Direct Initialize | payments.initialize() | Immediately on this call | You want to skip SmartHive's hosted UI and send the customer straight to a specific provider page | | API-First Charge | payments.apiCharge() | Immediately on this call | Server-side charges with no hosted page (mobile money, USSD, direct debit) |

⚠️ Never call checkout.createSession() and payments.initialize() for the same payment.

Each creates independent records. Calling both will:

  • Create two separate payment records for the same order
  • Present the customer with two different checkout URLs (one from SmartHive's hosted page, one from the provider directly)
  • Leave one record permanently stuck in pending — it will never receive a webhook or become verifiable
  • Cause payments.verify() and payments.getStatus() to return results for the wrong record

Pick one flow and use it exclusively end-to-end.


Checkout Flow (Recommended)

The checkout flow creates a hosted payment page. The customer pays on that page; your backend receives a webhook confirming the result.

Important — understand what createSession does and does not do:

checkout.createSession() creates a pre-payment container and returns a checkoutUrl. It does not create a payment transaction. No payment exists yet after this call.

A payment transaction is only created when the customer visits the checkoutUrl and submits their payment details on the hosted checkout page. Until that happens, calling payments.verify() with any reference from the session will return a 404 — there is nothing to verify yet.

The payment reference you use with verify() comes from either:

  • The webhook event (event.data.reference) — recommended
  • The returnUrl query string (?reference=txn_xxx) — after the customer is redirected back

Step 1 — Create the session (server-side)

const session = await smarthive.checkout.createSession({
  amount: '5000',        // GHS 50.00 in pesewas
  currency: 'GHS',
  countryCode: 'GH',
  platform: 'web',       // 'web' | 'mobile' | 'expo' | 'server'
  customerEmail: '[email protected]',
  customerName: 'Kwame Mensah',
  returnUrl: 'https://yourapp.com/order/complete',   // where to send the customer after payment
  callbackUrl: 'https://yourapp.com/webhooks/payments', // where SmartHive posts the result
  callerReference: 'ORDER-001',  // optional — your own order/transaction ID
  mode: 'sandbox',
});

// session.data.checkoutUrl     — send this to your frontend to redirect the customer
// session.data.sessionId       — a checkout session identifier (NOT a payment reference)
// session.data.callerReference — echoed back if you passed one ('ORDER-001')
//
// ⚠️  Do NOT pass session.data.sessionId to payments.verify() — it will not work.
//     A payment reference is only available after the customer pays (see steps below).

Step 2 — Redirect the customer

// Frontend: redirect to the checkout URL
window.location.href = session.data.checkoutUrl;
// or open in a new tab
window.open(session.data.checkoutUrl, '_blank');

Step 3A — Confirm payment via webhook (recommended)

Set up a webhook handler at the callbackUrl you provided. SmartHive posts a signed event when the customer pays. Use event.data.reference — this is the payment reference.

import express from 'express';
import { SmartHivePayments, WebhookSignatureError } from '@smarthivelabs-devs/payments';

const smarthive = new SmartHivePayments({ ... });
const app = express();

app.post(
  '/webhooks/payments',
  express.raw({ type: 'application/json' }), // ← raw body is required for signature verification
  (req, res) => {
    let event;
    try {
      event = smarthive.webhooks.constructEvent(
        req.body,
        req.headers['x-signature'] as string,
        process.env.SMARTHIVE_PAYMENTS_WEBHOOK_SECRET!,
      );
    } catch (err) {
      if (err instanceof WebhookSignatureError) {
        return res.status(400).send(`Webhook Error: ${err.message}`);
      }
      throw err;
    }

    switch (event.event) {
      case 'payment.success':
        // event.data.reference is the payment reference — use it to fulfill the order
        await fulfillOrder(event.data.reference);
        break;
      case 'payment.failed':
        await notifyCustomer(event.data.reference);
        break;
      case 'payout.success':
        await recordPayout(event.data.reference);
        break;
    }

    res.sendStatus(200);
  },
);

Step 3B — Confirm payment via returnUrl (alternative)

When the customer finishes on the checkout page, SmartHive redirects them to your returnUrl with ?reference=txn_xxx&status=success appended. Extract the reference from the query string and verify it server-side.

// Example: Express handler for GET /order/complete?reference=txn_xxx&status=success
app.get('/order/complete', async (req, res) => {
  const reference = req.query.reference as string | undefined;

  if (!reference) {
    // Customer may have navigated here directly — show a generic order page
    return res.render('order-pending');
  }

  // Verify with SmartHive to confirm the payment (don't trust query params alone)
  const result = await smarthive.payments.verify(reference);

  if (result.data.status === 'success') {
    await fulfillOrder(result.data.reference);
    return res.render('order-success', { reference: result.data.reference });
  }

  res.render('order-pending', { status: result.data.status });
});

Checkout Flow — Programmatic Launch (no hosted page redirect)

If you want to initiate payment entirely server-side — skipping the hosted checkout page — use checkout.launchSession() after createSession(). This creates the payment transaction immediately and returns a paymentReference you can verify right away.

// Step 1: Create the session container
const session = await smarthive.checkout.createSession({
  amount: '5000',
  currency: 'GHS',
  countryCode: 'GH',
  platform: 'server',
  customerEmail: '[email protected]',
  returnUrl: 'https://yourapp.com/order/complete',
  callbackUrl: 'https://yourapp.com/webhooks/payments',
  mode: 'sandbox',
});

// Step 2: Launch the session — this creates the payment transaction
const launch = await smarthive.checkout.launchSession(session.data.sessionId, {
  paymentMethod: 'card',     // or 'mobile_money', 'bank_transfer', etc.
  customerEmail: '[email protected]',
  callbackUrl: 'https://yourapp.com/webhooks/payments',
  returnUrl: 'https://yourapp.com/order/complete',
  mode: 'sandbox',
});

// launch.data.paymentReference is a real payment reference — safe to verify
console.log(launch.data.paymentReference); // 'txn_xxx' — use this with payments.verify()
console.log(launch.data.checkoutUrl);      // redirect the customer here to complete payment

// Step 3: After the customer pays, verify using paymentReference
const result = await smarthive.payments.verify(launch.data.paymentReference);
if (result.data.status === 'success') {
  await fulfillOrder(result.data.reference);
}

Initialize a Payment (Direct)

This is an alternative to checkout.createSession(), not an addition to it. If your app already calls createSession(), remove this call. Using both creates duplicate payment records — see Which Flow to Use above.

For simple integrations without a hosted checkout session:

const payment = await smarthive.payments.initialize({
  amount: '5000',
  currency: 'GHS',
  countryCode: 'GH',
  customerEmail: '[email protected]',
  paymentMethod: 'card',
  returnUrl: 'https://yourapp.com/order/complete',
  callbackUrl: 'https://yourapp.com/webhooks/payments',
  reference: 'ORDER-001',          // optional — your own reference
  mode: 'sandbox',
});

// Redirect customer to payment.data.checkoutUrl
console.log(payment.data.reference);         // your caller reference (or 'ORDER-001')
console.log(payment.data.internalReference); // SmartHive's reference ('txn_xxx')

// After the customer pays, verify using either reference
const result = await smarthive.payments.verify(payment.data.reference);

Verify a Payment

Call verify() only after the customer has completed payment. Pass the payment reference from the webhook event.data.reference, the returnUrl query string ?reference=..., or the paymentReference returned by checkout.launchSession().

// Full verification (server-side, after webhook or redirect)
const payment = await smarthive.payments.verify('txn_xxx');
console.log(payment.data.status);    // 'success' | 'failed' | 'pending'
console.log(payment.data.amount);    // '5000'
console.log(payment.data.paidAt);    // '2026-01-01T11:45:00.000Z'

// Lightweight status check (for polling — cheaper than full verify)
const status = await smarthive.payments.getStatus('txn_xxx');
console.log(status.data.status);

What reference to pass:

  • After checkout.createSession() + customer pays on hosted page → use event.data.reference from webhook or ?reference= from returnUrl query string
  • After checkout.createSession() with callerReference → you can also verify with your own reference: smarthive.payments.verify('ORDER-001')
  • After checkout.launchSession() → use launch.data.paymentReference
  • After payments.initialize() with reference → use your reference directly: smarthive.payments.verify('ORDER-001')

Webhooks

Use constructEvent to verify and parse incoming webhooks. Always use the raw request body — not parsed JSON.

Express

import express from 'express';
import { SmartHivePayments, WebhookSignatureError } from '@smarthivelabs-devs/payments';

const smarthive = new SmartHivePayments({ ... });
const app = express();

app.post(
  '/webhooks/payments',
  express.raw({ type: 'application/json' }), // ← raw body required
  (req, res) => {
    let event;
    try {
      event = smarthive.webhooks.constructEvent(
        req.body,
        req.headers['x-signature'] as string,
        process.env.SMARTHIVE_PAYMENTS_WEBHOOK_SECRET!,
      );
    } catch (err) {
      if (err instanceof WebhookSignatureError) {
        return res.status(400).send(`Webhook Error: ${err.message}`);
      }
      throw err;
    }

    switch (event.event) {
      case 'payment.success':
        await fulfillOrder(event.data.reference);
        break;
      case 'payment.failed':
        await notifyCustomer(event.data.reference);
        break;
      case 'payout.success':
        await recordPayout(event.data.reference);
        break;
    }

    res.sendStatus(200);
  },
);

Static usage (without client instance)

import { SmartHivePayments } from '@smarthivelabs-devs/payments';

const event = SmartHivePayments.webhooks.constructEvent(rawBody, signature, secret);

Webhook Event Types

| Event | Description | |---|---| | payment.success | Payment completed | | payment.failed | Payment failed | | payment.pending | Awaiting confirmation | | payment.abandoned | Customer abandoned checkout | | payout.success | Payout completed | | payout.failed | Payout failed | | mandate.approved | Direct debit mandate approved | | mandate.declined | Mandate declined by customer | | refund.success | Refund processed | | coupon.redeemed | Coupon applied and usage incremented after payment |


Direct Debit

Charge a customer without re-entering payment details, after a one-time preapproval. Two providers are supported — they work differently:

| Provider | Method | Countries | How approval works | |---|---|---|---| | Paystack | Card authorization | GH, NG | Customer pays on a hosted page once; you receive a card authorizationCode | | Hubtel | Mobile money USSD/OTP | GH | Customer approves via USSD prompt or OTP on their phone; store their MSISDN as the authorizationCode |

Hubtel note: Direct debit requires your server IP to be whitelisted by Hubtel. Contact your Retail System Engineer before going live.

Step 1 — Create Mandate

Paystack (card authorization — customer redirected to hosted page):

const mandate = await smarthive.directDebit.createMandate({
  amount: '5000',
  currency: 'GHS',
  countryCode: 'GH',
  customerEmail: '[email protected]',
  callbackUrl: 'https://yourapp.com/webhooks/payments',
  returnUrl: 'https://yourapp.com/direct-debit/complete',
  providerCode: 'paystack',
  mode: 'sandbox',
});

// Redirect customer to Paystack's authorization page
console.log(mandate.data.checkoutUrl);
// After they authorize, receive a card authorizationCode via mandate.approved webhook

Hubtel (mobile money USSD/OTP — no redirect, customer approves on their phone):

const mandate = await smarthive.directDebit.createMandate({
  amount: '5000',
  currency: 'GHS',
  countryCode: 'GH',
  customerEmail: '[email protected]',
  customerPhone: '233244123456',   // required for Hubtel
  callbackUrl: 'https://yourapp.com/webhooks/payments',
  returnUrl: 'https://yourapp.com/direct-debit/complete',
  providerCode: 'hubtel',
  metadata: { mobileMoneyProvider: 'mtn' },  // mtn | vodafone | airteltigo
  mode: 'sandbox',
});

// No checkoutUrl — customer receives a USSD prompt or OTP on their phone
// Store mandate.data.rawResponse.authorizationCode (the MSISDN) for future charges

Step 2 — Wait for Webhook

When the customer approves, you receive a mandate.approved webhook. Store the authorizationCode against the customer record.

  • Paystack: event.data.authorizationCode — a card token (e.g. AUTH_xxx)
  • Hubtel: store mandate.data.rawResponse.authorizationCode — the customer's MSISDN (e.g. '233244123456')

Step 3 — Charge (no interaction required)

// Paystack
const charge = await smarthive.directDebit.chargeMandate({
  amount: '5000',
  currency: 'GHS',
  countryCode: 'GH',
  customerEmail: '[email protected]',
  authorizationCode: 'AUTH_xxx',           // card token from webhook
  providerCode: 'paystack',
  reference: 'SUBSCRIPTION-JAN-2026',
  mode: 'live',
});

// Hubtel
const charge = await smarthive.directDebit.chargeMandate({
  amount: '5000',
  currency: 'GHS',
  countryCode: 'GH',
  customerEmail: '[email protected]',
  authorizationCode: '233244123456',       // MSISDN stored from mandate creation
  providerCode: 'hubtel',
  metadata: { mobileMoneyProvider: 'mtn' },
  reference: 'SUBSCRIPTION-JAN-2026',
  mode: 'live',
});

console.log(charge.data.status); // 'success' | 'pending'

Payouts

Send money to mobile wallets or bank accounts.

Step 1 — Create Recipient

const recipient = await smarthive.payouts.createRecipient({
  type: 'mobile_money',          // 'mobile_money' | 'bank_account'
  name: 'Kwame Mensah',
  accountNumber: '0244123456',
  countryCode: 'GH',
  currency: 'GHS',
  mode: 'live',
});

// Store recipient.data.recipientCode for future payouts

Step 2 — Send Payout

const payout = await smarthive.payouts.send({
  amount: '10000',               // GHS 100.00 in pesewas
  currency: 'GHS',
  countryCode: 'GH',
  recipientCode: recipient.data.recipientCode,
  payoutMethod: 'mobile_money',
  reason: 'Contest winnings',
  mode: 'live',
});

console.log(payout.data.status); // 'pending' (confirmed via payout.success webhook)

Bank Verification

// List available banks
const banks = await smarthive.payments.listBanks({ countryCode: 'GH', mode: 'sandbox' });

// Resolve account number → account name before initiating transfer
const account = await smarthive.payments.verifyBankAccount({
  accountNumber: '0123456789',
  bankCode: 'GCB',
  countryCode: 'GH',
  currency: 'GHS',
  mode: 'sandbox',
});
console.log(account.data.accountName); // 'Kwame Mensah'

API-First Charge (No Hosted Checkout)

Charge a mobile money wallet directly from your server — useful for subscription billing.

const charge = await smarthive.payments.apiCharge({
  amount: '5000',
  currency: 'GHS',
  countryCode: 'GH',
  method: 'mobile_money',
  customerEmail: '[email protected]',
  customerPhone: '233244123456',
  mobileMoneyProvider: 'mtn',   // 'mtn' | 'vodafone' | 'telecel' | 'airteltigo'
  callbackUrl: 'https://yourapp.com/webhooks/payments',
  returnUrl: 'https://yourapp.com/order/complete',
  mode: 'sandbox',
});

Idempotency

Pass an idempotencyKey to safely retry requests without double-charging:

const payment = await smarthive.payments.initialize(
  { amount: '5000', currency: 'GHS', ... },
  'order-001-attempt-1',  // idempotency key
);

Error Handling

All errors extend SmartHiveError with statusCode, code, and optional correlationId:

import {
  SmartHiveError,
  AuthenticationError,
  ValidationError,
  RateLimitError,
  NotFoundError,
  PaymentError,
  WebhookSignatureError,
} from '@smarthivelabs-devs/payments';

try {
  const payment = await smarthive.payments.initialize({ ... });
} catch (err) {
  if (err instanceof AuthenticationError) {
    // Invalid or expired API key
  } else if (err instanceof ValidationError) {
    // Missing or invalid request fields
  } else if (err instanceof RateLimitError) {
    // Rate limited — err.retryAfter gives seconds to wait
    console.log(`Retry after ${err.retryAfter}s`);
  } else if (err instanceof NotFoundError) {
    // Resource not found — if verifying, the payment may not exist yet
  } else if (err instanceof PaymentError) {
    // Payment-specific error (400, 402, 409, 500, 502)
    console.log(err.statusCode, err.code, err.correlationId);
  } else if (err instanceof SmartHiveError) {
    // Any other SDK error
  }
}

Health Check

Verify your API key is valid:

const health = await smarthive.payments.health();
// Throws AuthenticationError if the API key is invalid

TypeScript

All request params and responses are fully typed. Import types directly:

import type {
  PaymentInitializeParams,
  PaymentInitializeResponse,
  CheckoutSessionParams,
  CheckoutSessionResponse,
  CheckoutLaunchParams,
  CheckoutLaunchResponse,
  DirectDebitMandateParams,
  DirectDebitChargeParams,
  PayoutParams,
  SmartHiveEvent,
  SmartHiveEventType,
  PaymentMode,
  // Coupon types
  CreateCouponParams,
  UpdateCouponParams,
  Coupon,
  CouponValidateParams,
  CouponValidateResponse,
  CouponStatsResponse,
  ListCouponsParams,
  CouponDiscountType,
  CouponApplicableTo,
} from '@smarthivelabs-devs/payments';

Common Mistakes

Calling createSession and payments.initialize together

The most frequent integration error. See Which Flow to Use. Choose one.

Calling payments.verify() or payments.getStatus() immediately after createSession()

// ❌ WRONG — this will always return 404
const session = await smarthive.checkout.createSession({ ... });
const status = await smarthive.payments.getStatus(session.data.sessionId); // 404

// ✅ CORRECT — verify only after the customer has paid
// Get the reference from your webhook event or from ?reference= on your returnUrl
const result = await smarthive.payments.verify(referenceFromWebhookOrReturnUrl);

At the time createSession() returns, no transaction exists yet. A transaction is only created when the customer visits the checkoutUrl and submits payment. Calling verify or getStatus with a session ID (chs_xxx) or any reference before that point returns 404 — there is nothing to find.

Polling getStatus() on a 404 in a retry loop

A 404 from getStatus() means the transaction does not exist — not that it is still pending. Retrying indefinitely on 404 creates an infinite loop.

// ❌ WRONG — 404 does not mean "pending, try again"
while (true) {
  const status = await smarthive.payments.getStatus(reference);
  if (status.data.status === 'success') break;
  await sleep(3000); // loops forever if reference never existed
}

// ✅ CORRECT — stop on 404; it means the transaction was never created
import { NotFoundError } from '@smarthivelabs-devs/payments';
try {
  const status = await smarthive.payments.getStatus(reference);
  if (status.data.status === 'success') { /* fulfill */ }
} catch (err) {
  if (err instanceof NotFoundError) {
    // Transaction not found — do not retry; investigate why it was never created
  }
}

Passing sessionId to payments.verify()

// ❌ WRONG — session IDs (chs_xxx) are not payment references
const result = await smarthive.payments.verify(session.data.sessionId); // 404

// ✅ CORRECT — use the reference from the webhook or from ?reference= on your returnUrl
const result = await smarthive.payments.verify(event.data.reference);
// or if you passed callerReference: 'ORDER-001' to createSession:
const result = await smarthive.payments.verify('ORDER-001');

Coupons

Create and manage discount codes. Discounts are always applied server-side — the checkout session stores the discounted amount so the client never controls pricing.

Create a coupon

const coupon = await smarthive.coupons.create({
  name: 'Summer Sale',
  discountType: 'percentage',
  discountValue: 2000,        // 2000 basis points = 20%
  applicableTo: 'all',        // 'all' | 'checkout_only' | 'subscriptions_only'
  maxUses: 100,               // -1 = unlimited
  maxUsesPerCustomer: 1,
  expiresAt: '2026-12-31T23:59:59Z',
  // couponCode omitted → auto-generates e.g. "COUP_3F8A"
});

console.log(coupon.data.couponCode);  // 'COUP_3F8A'

Fixed-amount coupon (currency-scoped):

const fixedCoupon = await smarthive.coupons.create({
  name: 'GHS 10 Off',
  discountType: 'fixed',
  discountValue: 1000,   // GHS 10.00 in minor units
  currency: 'GHS',       // required for fixed-type — enforced on every redemption
  applicableTo: 'checkout_only',
});

Validate a coupon (dry run)

Preview the discount without incrementing usage — call this before checkout to show the customer their savings.

const preview = await smarthive.coupons.validate('SUMMER20', {
  amountMinor: 5000,           // GHS 50.00
  currency: 'GHS',
  customerId: 'user_001',
  applicableTo: 'checkout',
});

// preview.data.valid                — true/false
// preview.data.discountAmountMinor  — 1000  (GHS 10.00 off)
// preview.data.finalAmountMinor     — 4000  (GHS 40.00 to pay)

Apply at checkout

Pass couponCode when creating the checkout session. The backend validates the coupon, increments usage, and stores the discounted amount on the session. The customer only pays the discounted total.

Set allowCouponInput: true to show a coupon input field on the hosted checkout page so customers can enter codes themselves. Omit it (default: false) and no coupon UI appears — only pre-applied coupons via couponCode take effect.

const session = await smarthive.checkout.createSession({
  amount: '5000',
  currency: 'GHS',
  countryCode: 'GH',
  couponCode: 'SUMMER20',     // optional — pre-apply a coupon at session creation
  allowCouponInput: true,     // optional — show coupon input on hosted checkout page
  customerEmail: '[email protected]',
  returnUrl: '...',
  callbackUrl: '...',
  mode: 'sandbox',
});

// session.data.originalAmountMinor  — '5000' (what was requested)
// session.data.discountAmountMinor  — '1000' (how much was deducted)
// session.data.amountMinor is now   — '4000' (what the customer pays)

Apply at subscription creation:

await smarthive.subscriptions.create({
  planCode: 'plan_pro_monthly',
  customerId: 'user_001',
  authorizationCode: 'AUTH_xxx',
  couponCode: 'SUMMER20',  // discount applied to first charge
  mode: 'sandbox',
});

CRUD operations

// List coupons (optionally filter by active state)
const list = await smarthive.coupons.list({ isActive: true, limit: 20, offset: 0 });

// Get a single coupon
const coupon = await smarthive.coupons.get('SUMMER20');

// Update (partial — code, discountType, and discountValue are immutable)
await smarthive.coupons.update('SUMMER20', {
  maxUses: 200,
  expiresAt: '2027-01-01T00:00:00Z',
  isActive: false,
});

// Delete (soft-delete — existing redemptions are unaffected)
await smarthive.coupons.delete('SUMMER20');

Stats

const stats = await smarthive.coupons.stats('SUMMER20');

// stats.data.coupon         — full CouponResponse
// stats.data.totalUses      — number of redemptions
// stats.data.recentUsages   — last redemptions with customer + amounts

Coupon webhook event

Add a coupon.redeemed handler to your webhook listener:

case 'coupon.redeemed':
  // event.data.couponCode          — 'SUMMER20'
  // event.data.discountAmountMinor — 1000
  // event.data.finalAmountMinor    — 4000
  // event.data.currency            — 'GHS'
  // event.data.sessionId           — checkout session that used the coupon
  await recordCouponRedemption(event.data);
  break;

Validation errors

The backend enforces all constraints and returns a descriptive 400 error when validation fails:

| Error | Cause | |---|---| | Coupon is inactive | isActive is false | | Coupon has expired | expiresAt is in the past | | Coupon is only valid for subscriptions | Used on checkout but applicableTo = subscriptions_only | | Coupon currency does not match | Fixed coupon currency ≠ request currency | | Order amount is below the minimum for this coupon | amountMinor < minOrderAmount | | Coupon usage limit reached | currentUses >= maxUses | | Coupon usage limit reached for your account | Per-customer maxUsesPerCustomer hit | | Coupon has already been used | Single-use coupon already redeemed by this customer | | Coupon is not valid for this plan | Subscription plan not in planCodes list |

TypeScript types

import type {
  CreateCouponParams,
  UpdateCouponParams,
  Coupon,
  CouponValidateParams,
  CouponValidateResponse,
  CouponStatsResponse,
  ListCouponsParams,
  CouponDiscountType,   // 'percentage' | 'fixed'
  CouponApplicableTo,   // 'all' | 'checkout_only' | 'subscriptions_only'
} from '@smarthivelabs-devs/payments';

Subscriptions

Recurring billing — create plans, attach subscribers, and manage the full lifecycle from your server.

Create a plan

const plan = await smarthive.subscriptions.plans.create({
  name: 'Pro Monthly',
  amount: 5000,           // in the smallest currency unit (kobo, pesewas, cents)
  currency: 'GHS',
  countryCode: 'GH',
  interval: 'monthly',   // daily | weekly | monthly | quarterly | yearly
  trialPeriodDays: 7,     // 0 = no trial
  gracePeriodDays: 3,     // days after failed payment before access is revoked
  entitlements: ['read', 'download', 'api_access'],  // named feature keys
  description: 'Full access to Pro features',
  mode: 'sandbox',
});

console.log(plan.data.planCode); // e.g. "plan_pro_monthly_abc123"

Create a subscription

Attach a customer to a plan. Pass authorizationCode (from a prior direct-debit mandate or card authorization) so the worker can charge them automatically on each renewal.

const subscription = await smarthive.subscriptions.create({
  planCode: 'plan_pro_monthly_abc123',
  customerId: 'user_001',         // your own user ID — used for SDK lookup
  customerEmail: '[email protected]',
  customerName: 'Ama Mensah',
  authorizationCode: 'AUTH_xxx',  // from a mandate.approved webhook
  providerCode: 'paystack',       // provider that issued the auth code
  mode: 'sandbox',
});

console.log(subscription.data.subscriptionCode); // e.g. "sub_abc123"
console.log(subscription.data.status);           // "trialing" | "active"

Look up a customer's subscription

// Single plan — used by SDK hooks to gate access
const lookup = await smarthive.subscriptions.lookup({
  planCode: 'plan_pro_monthly_abc123',
  customerId: 'user_001',
});

// lookup.data.hasAccess     — true when active, trialing, or in grace period
// lookup.data.isInGracePeriod
// lookup.data.entitlements  — ['read', 'download', 'api_access']
// lookup.data.subscription  — full Subscription object

// Batch — check multiple plans in one request
const batch = await smarthive.subscriptions.lookupBatch({
  planCodes: ['plan_basic', 'plan_pro'],
  customerId: 'user_001',
});
// batch.data['plan_pro'].hasAccess → true/false

Lifecycle management

// Pause — optionally specify days; resumes automatically when pauseEndsAt passes
await smarthive.subscriptions.pause('sub_abc123', { pauseDays: 30 });

// Resume early
await smarthive.subscriptions.resume('sub_abc123');

// Cancel at end of current period (default)
await smarthive.subscriptions.cancel('sub_abc123');

// Cancel immediately
await smarthive.subscriptions.cancel('sub_abc123', { immediate: true });

Charge history

const charges = await smarthive.subscriptions.charges('sub_abc123');
// charges.data — SubscriptionCharge[] with status, attemptNumber, chargedAt

Plan management

// List all plans for your app
const plans = await smarthive.subscriptions.plans.list();

// Get a single plan
const plan = await smarthive.subscriptions.plans.get('plan_pro_monthly_abc123');

// Update (partial)
await smarthive.subscriptions.plans.update('plan_pro_monthly_abc123', {
  gracePeriodDays: 5,
  entitlements: ['read', 'download', 'api_access', 'priority_support'],
});

// Soft delete (existing subscribers are unaffected)
await smarthive.subscriptions.plans.delete('plan_pro_monthly_abc123');

Webhook events

| Event | Fired when | |---|---| | subscription.created | New subscriber created | | subscription.activated | Trial ended, subscription went active | | subscription.charged | Recurring charge succeeded | | subscription.charge_failed | Charge failed (entering grace period or retry) | | subscription.paused | Subscription paused | | subscription.resumed | Subscription resumed | | subscription.cancelled | Cancelled or max retries exhausted | | subscription.trial_ending | 72 hours before trial ends | | subscription.trial_ended | Trial period just ended |

How automatic charging works

  1. When a subscription is created with an authorizationCode, the worker stores it and sets next_charge_at based on the plan interval.
  2. The SubscriptionWorker runs hourly and finds all subscriptions due for charge.
  3. On success → status stays active, next_charge_at advances by one interval, subscription.charged webhook fires.
  4. On failure → retry_count increments, status becomes past_due (grace period begins), subscription.charge_failed fires. After maxRetryCount failures → status becomes expired, subscription.cancelled fires.

TypeScript types

import type {
  SubscriptionPlan,
  Subscription,
  SubscriptionCharge,
  SubscriptionStatus,   // 'pending' | 'trialing' | 'active' | 'past_due' | 'paused' | 'cancelled' | 'expired'
  SubscriptionInterval, // 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'
  CreatePlanDto,
  CreateSubscriptionDto,
  LookupBatchResponse,
} from '@smarthivelabs-devs/payments';

Support

Contact Smart Hive Labs or open an issue in the workspace repository.