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

@pulsebyshiga/node

v0.2.1

Published

Pulse Collect backend SDK — collection sessions, orders, and webhook signature verification

Readme

@pulsebyshiga/node

The official Node.js SDK for the Pulse API — quotes, offramp and onramp orders, bank name-enquiry, collection sessions, and webhook verification. Idempotent requests, automatic retries, and fully-typed responses and errors are built in.

Server-side only: your sk_* key and user identity (BVN/NIN) never leave your backend. The browser and mobile SDKs receive only a short-lived, single-order session token (cs_*).

Pre-release (0.2.0). The public API may still change. Quotes, offramp/onramp orders, and bank name-enquiry map 1:1 to the live engine contract (Swagger).

Requirements

  • Node.js ≥ 18.17
  • ESM (import) — this package does not ship a CommonJS build

Installation

npm install @pulsebyshiga/node
# pnpm add @pulsebyshiga/node · yarn add @pulsebyshiga/node

Quickstart

import Pulse, { useApiKey } from '@pulsebyshiga/node';

const pulse = new Pulse(useApiKey(process.env.PULSE_API_KEY), {
  webhookSecret: process.env.PULSE_WEBHOOK_SECRET,
});

// Lock a price, then create an order against it.
const quote = await pulse.quotes.create({
  amount: '100.50',
  source_currency: 'USDC',
  destination_currency: 'NGN',
  network: 'BASE',
});

useApiKey(...) declares the server credential (a bare string still works for back-compat). The key is sent as X-API-Key (the engine's scheme) and as a Bearer token (the session surface's scheme) — one key, both means. The default host is https://engine-api.shiga.io; override it with baseUrl for local development or a future sandbox.

Core concept — quote first

Everything starts with a locked price. Create a quote, then create an order from quote.id before expires_at passes. Amounts are decimal strings; networks are UPPERCASE.

Just the rate, no orderrates.preview wraps a quote for display; nothing settles and no order is created:

const rate = await pulse.rates.preview({
  from: 'USDC',
  to: 'NGN',
  amount: '100.00',
  network: 'BASE',
});
// → { rate, youSend, youReceive, expiresAt, quoteId }
// Proceed to an order with quoteId to transact at exactly this locked rate.

Offramp — crypto in, fiat out

The collection direction. Verify the payout account (name enquiry), then create the order:

const quote = await pulse.quotes.create({
  amount: '100.50',
  source_currency: 'USDC',
  destination_currency: 'NGN',
  network: 'BASE', // BASE | POLYGON | ARBITRUM | OPTIMISM | ETHEREUM | BSC
});

const account = await pulse.banks.resolve({ account_number: '0123456789', bank_code: '000013' });

const order = await pulse.offramp.create({
  quote_id: quote.id,
  customer_id: 'your_user_123',
  customer_name: account.account_name,
  customer_email: '[email protected]',
  account_number: account.account_number,
  account_name: account.account_name,
  bank_code: account.bank_code,
  bank_name: account.bank_name,
});
// Show order.deposit_address — the user sends quote.source_amount there.
// Poll pulse.offramp.retrieve(order.id) until a terminal status.

Onramp — fiat in, crypto out

const quote = await pulse.quotes.create({
  amount: '50000.00',
  source_currency: 'NGN',
  destination_currency: 'USDC',
  network: 'BASE',
});
const order = await pulse.onramp.create({
  quote_id: quote.id,
  customer_id: 'your_user_123',
  customer_name: 'Ada Lovelace',
  customer_email: '[email protected]',
  destination_address: '0xUserWallet…',
});
// order.account is the virtual bank account to pay (amount + expires_at).
// Poll pulse.onramp.retrieve(order.id) until a terminal status.

Banks

  • pulse.banks.list() — supported banks (name + NIP code).
  • pulse.banks.resolve({ account_number, bank_code }) — name enquiry; run it before creating an offramp order.

Collection sessions

The surface the embedded components run on. Create a session server-side and hand the cs_* token to your frontend — nothing else.

// Naira gate — funds settle to the user's named virtual account.
// First use provisions the account from BVN/NIN; returning users pass the reference.
const session = await pulse.collectionSessions.create(
  {
    gate: 'naira',
    target: { currency: 'NGN', amount: '1000000.00' },
    asset: 'USDT',
    network: 'solana',
    user_ref: 'your_user_123',
    account: { provision: true, bvn: '...', nin: '...' },
  },
  { idempotencyKey: `fund-${depositId}` },
);

// USD gate — funds settle to your omnibus account; no per-user identity.
const usdSession = await pulse.collectionSessions.create({
  gate: 'usd',
  target: { currency: 'USD', amount: '500.00' },
  asset: 'USDC',
  network: 'base',
});

// Hand session.session_token (cs_*) to your frontend. Nothing else.

Direction. Sessions default to offramp (crypto in → fiat out). Pass direction: 'onramp' for the reverse — the user pays a virtual bank account (carried on the order as pay_account) and receives crypto at a wallet. The embedded component renders the matching UI automatically (bank-transfer instructions instead of a deposit address/QR).

const onrampSession = await pulse.collectionSessions.create({
  gate: 'naira',
  direction: 'onramp',
  target: { currency: 'NGN', amount: '50000.00' },
  asset: 'USDT',
  network: 'base',
  user_ref: 'your_user_123',
  account: { provision: false, virtual_account_ref: 'va_...' },
});

Query orders.

const order = await pulse.collectionOrders.retrieve(orderId); // ground truth for one order
const page = await pulse.collectionOrders.list({ gate: 'naira', status: 'completed', limit: 50 });
const relocked = await pulse.collectionOrders.refreshQuote(orderId); // new quote window for an expired order

Webhooks

Deliveries are signed with a Pulse-Signature header:

Pulse-Signature: t=<unix-seconds>,v1=<hex hmac-sha256 of "<t>.<raw body>">

Verification requires the raw request body — do not parse JSON first.

import express from 'express';

app.post('/webhooks/pulse', express.raw({ type: '*/*' }), async (req, res) => {
  const event = await pulse.webhooks.verifyOnce(req.body, req.header('pulse-signature') ?? '');
  if (event === null) return res.sendStatus(200); // redelivery — already processed

  switch (event.type) {
    case 'disbursement.completed':
      await creditPosition(event.data.order_id, event.data.disbursement.settlement_ref);
      break;
    case 'collection.amount.underpaid':
      await handleShortfall(event.data); // { expected, received, asset, network, tx_hash }
      break;
  }
  res.sendStatus(200);
});

| Method | Behavior | | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | webhooks.verify(payload, header, secret?, options?) | Constant-time HMAC check + replay-window check (default tolerance 300 s). Returns the typed event or throws SignatureVerificationError. | | webhooks.verifyOnce(payload, header, secret?, options?) | Everything verify does, plus de-duplication on the event id. Returns null for a redelivery. |

Delivery is at-least-once: handlers must tolerate duplicates. verifyOnce de-duplicates in-process by default; multi-instance deployments must back it with a shared store:

import Pulse, { type ProcessedEventStore } from '@pulsebyshiga/node';

const store: ProcessedEventStore = {
  has: (id) => redis.exists(`pulse:evt:${id}`).then(Boolean),
  add: (id) => redis.set(`pulse:evt:${id}`, '1', 'EX', 86_400).then(() => undefined),
};

const pulse = new Pulse(apiKey, { webhookSecret, webhookEventStore: store });

Event types

| Event | Fires when | | ------------------------------------------ | -------------------------------------------------------------------------------------------- | | collection.order.created | Session created; the full order is attached. | | collection.deposit.detected | On-chain deposit seen, not yet confirmed. | | collection.deposit.confirmed | Deposit confirmed at the required depth. | | collection.deposit.wrong_chain | Funds arrived on a network other than the order's; the order stays payable on the right one. | | collection.amount.underpaid / overpaid | Confirmed amount differs from the quote; carries expected vs received. | | disbursement.completed | Fiat settled. The only event that credits a user. | | collection.order.expired | Quote window elapsed without payment. | | collection.order.failed | Terminal failure; carries a reason. |

Credit positions from disbursement.completed only. Client-side callbacks such as the embed's onSuccess are UX signals and can be spoofed; the signed webhook cannot.

Reliability

  • Idempotency — every POST carries an Idempotency-Key (yours via idempotencyKey, otherwise a generated UUID), so retries can never double-create an order.
  • Retries — network errors, HTTP 429, and 5xx are retried with exponential backoff (2 retries, 250 ms base delay by default). Other 4xx responses throw immediately.
  • Identification, not telemetry — every request carries a static X-Pulse-SDK: pulse-node/<version> node/<version> (<platform>) header so the API can measure adoption and error rates per SDK version. The SDK never emits telemetry from your infrastructure.

Configuration

new Pulse(apiKey, options?)

| Option | Type | Default | Description | | ------------------- | --------------------- | ----------------------- | ------------------------------------------------------------------- | | baseUrl | string | derived from key prefix | API host override (local development, staging). | | maxRetries | number | 2 | Retry count for network errors / 429 / 5xx. | | retryDelayMs | number | 250 | Base backoff delay, doubled per attempt. | | fetchImpl | FetchLike | global fetch | Injectable transport for tests/instrumentation. | | webhookSecret | string | — | Default secret for webhooks.verify*; can also be passed per call. | | webhookEventStore | ProcessedEventStore | in-memory | Shared de-duplication store for verifyOnce. |

Error handling

All errors extend PulseError:

| Class | Thrown when | Notable fields | | ---------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | | PulseApiError | API returned a non-retryable error, or retries were exhausted | status, code (e.g. network_not_enabled), requestId | | PulseConnectionError | Request never produced an HTTP response | — | | SignatureVerificationError | Webhook signature invalid, malformed, or outside the replay window | — |

import { PulseApiError } from '@pulsebyshiga/node';

try {
  await pulse.collectionSessions.create(params);
} catch (error) {
  if (error instanceof PulseApiError && error.code === 'network_not_enabled') {
    // asset/network is disabled in your partner configuration
  } else {
    throw error;
  }
}

TypeScript

Written in TypeScript; every request, response, webhook event, and error is fully typed and exported (CollectionOrder, WebhookEvent, PartnerConfig, Network, …). The webhook event union discriminates on type, so a switch narrows event.data automatically.

Security model

  • sk_* keys and BVN/NIN are server-to-server only — never ship them to a browser or mobile app. Clients receive only the short-lived, single-order session_token (cs_*).
  • Verify every webhook before acting on it; verification is constant-time and replay-bounded.
  • Credit users exclusively from the signed disbursement.completed event.

Related packages

License

MIT © Shiga Digital