@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/paymentsRequires 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()andpayments.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()andpayments.getStatus()to return results for the wrong recordPick 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
createSessiondoes and does not do:
checkout.createSession()creates a pre-payment container and returns acheckoutUrl. 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
checkoutUrland submits their payment details on the hosted checkout page. Until that happens, callingpayments.verify()with any reference from the session will return a 404 — there is nothing to verify yet.The payment
referenceyou use withverify()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 callscreateSession(), 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 → useevent.data.referencefrom webhook or?reference=from returnUrl query string- After
checkout.createSession()withcallerReference→ you can also verify with your own reference:smarthive.payments.verify('ORDER-001')- After
checkout.launchSession()→ uselaunch.data.paymentReference- After
payments.initialize()withreference→ 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 webhookHubtel (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 chargesStep 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 payoutsStep 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 invalidTypeScript
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 + amountsCoupon 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/falseLifecycle 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, chargedAtPlan 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
- When a subscription is created with an
authorizationCode, the worker stores it and setsnext_charge_atbased on the plan interval. - The
SubscriptionWorkerruns hourly and finds all subscriptions due for charge. - On success → status stays
active,next_charge_atadvances by one interval,subscription.chargedwebhook fires. - On failure →
retry_countincrements, status becomespast_due(grace period begins),subscription.charge_failedfires. AftermaxRetryCountfailures → status becomesexpired,subscription.cancelledfires.
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.
