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

@bevingh/conduit-client

v0.1.0

Published

Typed client for Conduit's product-facing API: mobile money charges + OTP, card checkout, payment status, refund requests, pay-by-code, transaction/disbursement queries, and verification of Conduit's outbound webhook signature. Full product-facing surface

Readme

@bevingh/conduit-client

New-build, not a Phase 1 harvest. Built strictly to conduit/docs/api-reference.md (the live technical reference for the conduit repo), not to a remembered approximation of it — see conduit/PROGRESS.md's "Planned Initiative" section for the full background on why this package exists and what it's meant to replace (each integrator hand-rolling HTTP calls against the docs, which is exactly the kind of duplication that produced a real bug in conduit-admin-app — a network-code mismatch only caught by a later audit).

Purpose

Typed client for Conduit's product-facing API (/api/v1/*) — the surface a product integrates against to take payments through Conduit, not the /admin/* operator API (product/recipient/subaccount setup — that stays a conduit-admin-app / scripts/cli.js concern) or /api/v1/ussd/* (internal router-only, never called directly by product integrations).

| Field | Value | |---|---| | surfaceShape | pure_core_plus_express_adapter | | dependsOnPackages | @bevingh/payments (→ @bevingh/errors) | | status | built (not published to npm this session) |

Full surface, not an MVP subset: initiate charge (+ OTP flow), card checkout, payment status, refund requests, pay-by-code generate/lookup/cancel, transaction + disbursement queries, and verification of Conduit's outbound webhook signature. Every current integrator (bevin-events, HostelConnect's v2 backend, Academicx) is Node.js on the server side, so this one client covers the fleet as-is.

Install

npm install @bevingh/conduit-client

@bevingh/payments comes in as a dependency automatically (used only for its HMAC verification helper — no axios peer dependency is triggered by that).

Public API

import { createConduitClient } from '@bevingh/conduit-client';

const conduit = createConduitClient({
  baseUrl: process.env.CONDUIT_BASE_URL!,   // e.g. https://conduit.yourcompany.com
  apiKey: process.env.CONDUIT_API_KEY!,     // sk_test_... or sk_live_...
  // optional: fetchImpl — inject for tests or a non-global-fetch runtime
});

// Mobile money charge (async — listen for the payment.completed webhook, or poll getStatus)
const charge = await conduit.payments.initiateCharge(
  { phone: '0241234567', network: 'MTN', amount: 50, currency: 'GHS', paymentType: 'school-fees' },
  { idempotencyKey: crypto.randomUUID() }, // optional — 24h dedup window, scoped per product
);
// charge.status is "processing", or rarely "otp_required" — see below

if (charge.status === 'otp_required') {
  await conduit.payments.submitOtp(charge.ref, otpFromCustomer);
}

// Card checkout — returns a hosted Paystack URL to redirect the customer to
const checkout = await conduit.payments.initiateCheckout({
  amount: 100, currency: 'GHS', email: '[email protected]',
  callbackUrl: 'https://yourapp.com/payment/callback',
});

const status = await conduit.payments.getStatus(charge.ref);

const refund = await conduit.payments.requestRefund(charge.ref, 'Customer requested cancellation');
// queues for admin review — does not refund immediately

// Pay-by-Code (USSD, no smartphone needed)
const code = await conduit.codes.generate({ amount: 20, currency: 'GHS', description: 'School lunch top-up' });
const state = await conduit.codes.lookup(code.code);
await conduit.codes.cancel(code.code);

// Transactions
const page = await conduit.transactions.list({ status: 'completed', page: 1, limit: 20 });
const tx = await conduit.transactions.getByRef(charge.ref);
const disbursement = await conduit.transactions.getDisbursement(charge.ref);

| Export | Role | |---|---| | createConduitClient(config) | Returns { payments, codes, transactions, webhooks } | | ConduitApiError | Thrown on any non-2xx response — status, message, optional code (e.g. INVALID_OTP), optional correlationId, raw body | | verifyConduitWebhookSignature | Standalone HMAC-SHA256 verification for Conduit's outbound webhook (see below) |

Every resource method's parameter and return shape mirrors docs/api-reference.md field-for-field (see src/types.ts) — amount/dates as documented, no implicit unit conversion (Conduit's product-facing API is major units throughout, unlike @bevingh/money's integer-pesewa convention, which doesn't apply here).

Idempotency

initiateCharge, initiateCheckout, and codes.generate — the three POST routes Conduit itself supports replay-dedup on — take an optional second argument { idempotencyKey }, sent as the Idempotency-Key header. Same request within 24h (scoped per product on Conduit's side) replays the original response instead of creating a second charge. Not supplying one is fine; it's the caller's choice to opt in per the underlying route's support, not a blanket client-side retry policy.

Errors

import { ConduitApiError } from '@bevingh/conduit-client';

try {
  await conduit.payments.submitOtp(ref, otpCode);
} catch (err) {
  if (err instanceof ConduitApiError) {
    // err.status, err.message (= response body's "error"), err.code (e.g. "INVALID_OTP"), err.correlationId
  }
}

Matches docs/api-reference.md's error envelope ({ "error": "<message>" }, occasionally code/correlationId) exactly — this client does not invent its own error shape or retry policy on top of it.

Verifying Conduit's outbound webhook

Conduit signs its outbound payment.* events with X-Conduit-Signature — HMAC-SHA256 of the raw JSON body, keyed by the product's WEBHOOK_SECRET (docs/api-reference.md "Admin: Webhook Deliveries"). This is a different algorithm from the SHA-512 Conduit itself uses to verify inbound Paystack webhooks — do not reuse a sk_test_/sk_live_ API key or a Paystack secret here.

import { verifyConduitWebhookSignature } from '@bevingh/conduit-client';

// rawBody MUST be the original wire bytes (Buffer, or the exact pre-parse string) —
// re-serializing via JSON.stringify(req.body) changes the bytes and breaks the HMAC.
const valid = verifyConduitWebhookSignature(req.rawBody, req.headers['x-conduit-signature'], process.env.CONDUIT_WEBHOOK_SECRET!);

This is a thin wrapper over @bevingh/payments' verifyWebhookSignature(rawBody, signature, secret, { algorithm: 'sha256' }) — the crypto lives in that package, not duplicated here (@bevingh/payments' own doc-comment already names sha256 as the Conduit-outbound case).

Express adapter (@bevingh/conduit-client/adapters/express)

import { createConduitWebhookMiddleware } from '@bevingh/conduit-client/adapters/express';

// Capture the raw body BEFORE this middleware — same requirement as
// @bevingh/payments' createPaystackWebhookMiddleware. No JSON.stringify(req.body)
// fallback exists; a missing rawBody fails loud via next(TypeError), not silently.
app.use('/conduit-webhook', express.json({
  verify: (req, _res, buf) => { (req as any).rawBody = buf; },
}));

app.post('/conduit-webhook', createConduitWebhookMiddleware({
  secret: process.env.CONDUIT_WEBHOOK_SECRET!,
  handle: async (payload) => {
    // payload: ConduitWebhookPayload — only reachable after signature verification succeeds
  },
}), handler);

Responds 401 on a bad signature, calls next(TypeError) if req.rawBody was never captured, next(AppError) if secret is falsy — verification is structural, not optional, same principle as @bevingh/payments' inbound contract.

What this package does not do

  • Not the admin/operator API. Product/recipient/subaccount/markup setup, disbursement retries, refund approval, finance/ledger queries — all /admin/*, out of scope. That flow is conduit-admin-app (and, as a fallback, conduit/scripts/cli.js's "Onboard new product").
  • Not the USSD router integration. /api/v1/ussd is called by the Arkesel USSD router proxy only, authenticated with x-ussd-router/x-ussd-secret, never by a product's own backend.
  • No retry/backoff policy. A failed request throws ConduitApiError (or lets fetch's own network error propagate) — the caller decides whether and how to retry. This mirrors @bevingh/payments' driver style (inject behavior, don't hide it).
  • No built-in amount/currency conversion. Conduit's product-facing API takes and returns major units (e.g. 50.00 GHS) throughout — this client passes them through as-is.

Tests

npm run test -w @bevingh/conduit-client

Resource methods verified against a mocked fetchImpl (correct method/path/headers/body per docs/api-reference.md, idempotency header pass-through, query-string building with undefined params omitted, ConduitApiError field mapping on non-2xx). Webhook verification verified against real node:crypto HMAC-SHA256 signatures (valid, tampered, wrong secret, wrong algorithm, string vs. Buffer raw body) and the Express middleware against fake req/res/next (valid → 200 + handled payload, invalid signature → 401 + handle never called, missing rawBody → next(TypeError), case-insensitive header lookup). Not verified against a live Conduit deployment or a live webhook delivery — recommend a sk_test_* smoke test (one charge, one status poll, one webhook round-trip) before trusting this in a real integration.