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

@tap2pay/server

v0.1.3

Published

Tap2Pay server SDK — Tap2Access (entitlements, credentials, access events) and Tap2Pay (plans, subscriptions, payments, wallets) for merchant backends.

Readme

@tap2pay/server

The official Tap2Pay server SDK for merchant backends (Node 18+, TypeScript, zero runtime dependencies). It wraps the Tap2Pay Platform API — the API is the platform; this SDK just makes it pleasant to consume.

Two product domains, one client:

  • Tap2Access — people, credentials (cards/phones), entitlements, access events. "May this credential act here, now?"
  • Tap2Pay — plans, subscriptions, payments, wallets, settlements. "Can this transaction settle?"

Conventions: money is integer kobo (1_000_000 = ₦10,000). People are addressable by your own IDs — anywhere a person_id/personRef appears, your external_ref works.

Install & initialize

npm install @tap2pay/server
import { Tap2Pay } from "@tap2pay/server";

const t2p = new Tap2Pay({
  apiKey: process.env.TAP2PAY_API_KEY!, // t2p_test_… or t2p_live_…
  baseUrl: "https://api.tap2pay.example", // omit for local dev (:8090)
});

CommonJS works too — the package ships both builds:

const { Tap2Pay } = require("@tap2pay/server");

Test vs live: signup gives you a t2p_test_… key immediately — a full sandbox where payments use the sandbox processor. t2p_live_… keys unlock after KYC approval. The key decides the mode; the client is identical, so going live is swapping one env var.

Quickstart: subscriptions on the cards you issued

Tap2Pay issues NFC cards (or app credentials) to your customers. You define a plan once; subscribing a customer charges them through the payment processor and keeps their card working — renewals, expiry, and access all handled by the platform. (This is the Learn to Earn model.)

// One-time: register the customer and bind their issued card
await t2p.people.create({ full_name: "Amina Yusuf", external_ref: "STU-2026-0700" });
await t2p.credentials.createCard("STU-2026-0700", "04:C9:33:E2");

// One-time: define the plan (₦10,000/month for the Hub Access service)
const plan = await t2p.plans.create({
  name: "Hub Monthly",
  service_id: "SVC_…",
  amount: 1_000_000,
});

// Subscribe — first charge now, entitlement live immediately, auto-renews
const { payment, effects } = await t2p.subscriptions.create({
  person_id: "STU-2026-0700",
  plan_id: plan.id,
});
// payment.status === "APPROVED"
// effects.entitlement.valid_until → one month out
// The card now opens every door the service covers.

// Later: stop renewals (access runs out at period end on its own)
await t2p.subscriptions.cancel(effects.subscription!.id);

Managing entitlements directly (customer-managed billing)

If money settles outside Tap2Pay (e.g. payroll deduction), sync entitlement facts instead — PERIOD grants upsert in place, so re-sending state is safe:

await t2p.entitlements.create({
  person_id: "STU-2026-0700",
  service_id: "SVC_…",
  valid_until: "2026-09-30T23:00:00Z",
  source: "API",
  source_ref: "YOUR-SETTLEMENT-REF",
});

// Monthly cycle, up to 1000 at once:
await t2p.entitlements.bulk(students.map((s) => ({
  person_id: s.id, service_id: HUB, valid_until: monthEnd, source: "API",
})));

Webhooks

import { constructEvent, isAccessEvent, WebhookVerificationError } from "@tap2pay/server";

// One-time: subscribe (the secret is returned once — store it)
const endpoint = await t2p.webhookEndpoints.create({
  url: "https://your-backend/webhooks/tap2pay",
  event_types: ["access.granted", "access.denied", "entitlement.expired"],
});

// In your webhook route (use the RAW request body):
app.post("/webhooks/tap2pay", (req, res) => {
  try {
    const event = constructEvent(process.env.TAP2PAY_WEBHOOK_SECRET!, req.rawBody, {
      signature: req.headers["x-tap2pay-signature"] as string,
      timestamp: req.headers["x-tap2pay-timestamp"] as string,
    });
    // isAccessEvent types event.data as AccessEventPayload and covers both
    // decisions — a refused tap is worth recording too.
    if (isAccessEvent(event)) {
      // person_ref is YOUR identifier for this person, so you never need a
      // person_id -> your-id table. service_name is set on denies as well.
      await recordTap(event.data.person_ref, event.data.decision, event.data.service_name);
    }
    res.sendStatus(204);
  } catch (e) {
    if (e instanceof WebhookVerificationError) return res.sendStatus(401);
    throw e;
  }
});

Deliveries retry with backoff and may arrive more than once — key on event.id. Answer 2xx first and do the work after, or a slow handler turns into a retry storm of duplicates.

Live event stream

For a dashboard or a local tail -f of what the platform is doing, iterate the event feed directly — no public URL to expose, no endpoint to register:

const ctl = new AbortController();
for await (const ev of t2p.events.stream({ signal: ctl.signal })) {
  console.log(ev.occurred_at, ev.type, ev.data);
  if (ev.type === "access.denied") alertTheFrontDesk(ev.data);
}
// ctl.abort() — or `break` — closes the connection.

The stream has no cursor: anything published while you were disconnected is gone. It is a tail, not a delivery guarantee — keep webhooks for anything you bill, reconcile or audit on.

Everything else

t2p.people          // create · get · list · suspend · activate
t2p.credentials     // createCard · list · setStatus · createMobileActivation
t2p.services        // create · list · setResources
t2p.resources       // create · list
t2p.devices         // enroll (secret shown once) · list
t2p.entitlements    // create · bulk · list · revoke
t2p.accessEvents    // list (since / person / limit)
t2p.plans           // create · list · archive
t2p.subscriptions   // create · list · cancel
t2p.payments        // create · list · refund
t2p.wallets         // get · credit (counter top-up) · customers · org
t2p.charges         // refund (closed-loop terminal charges)
t2p.settlements     // create (payout) · list · getSchedule · setSchedule · setPayoutDestination
t2p.reports         // reconciliation · dashboard
t2p.setup           // get (activation state, derived from live data)
t2p.pricing         // get (your plan) · fees (what you were charged)
t2p.disputes        // list · summary · get · submitEvidence
t2p.notifications   // list · unreadCount · markRead · markAllRead · channels.*
t2p.apiKeys         // list · create · revoke
t2p.staff           // list · create · setRole · remove
t2p.webhookEndpoints// create · list
t2p.events          // stream (live SSE tail)

t2p.credentials.listAll() is the whole card fleet with balances and last-used times; t2p.wallets.customers({ below }) finds customers about to be turned away at a gate for an empty balance.

Notification preferences are deliberately absent — they are per-user email settings, and an API key has no user.

t2p.staff.create() provisions a member outright, password and all — for seeding an operator or a reporting account from a script. A human joining the team should come in through the dashboard's invite flow instead, so they set their own password and enrol their own 2FA.

The two flows

A card does one of two things. Both run on the same card and the same terminal.

Access by subscription — the customer holds a standing entitlement and a tap just opens:

const svc  = await t2p.services.create({ name: "Gym", entitlement_type: "PERIOD" });
const plan = await t2p.plans.create({ name: "Gym Monthly", service_id: svc.id, amount: 500000 });
await t2p.subscriptions.create({ person_id: "MEM-0042", plan_id: plan.id });

Pay on the go — the customer holds a balance and each tap costs money:

const svc = await t2p.services.create({ name: "Canteen", entitlement_type: "METERED", fee: 25000 });
await t2p.wallets.credit({ person_id: "MEM-0042", amount: 500000, reference: `topup-${Date.now()}` });

reference is the idempotency key and must be unique — that is what stops a retry crediting twice. Customers can also top up themselves in the Tap2Pay app.

t2p.setup.get() returns what is built so far and what to do next, derived from live counts — useful for driving your own onboarding UI.

Money you hold vs money you have earned

wallets.customers() reports float: money customers loaded and have not yet spent. It is a liability, not revenue — it becomes yours as they tap. wallets.org() and reports.dashboard() report settleable revenue. The two are different numbers and the SDK never mixes them.

Errors

All failures throw Tap2PayError with status, code, and message:

try {
  await t2p.subscriptions.create({ person_id: ref, plan_id });
} catch (e) {
  if (e instanceof Tap2PayError && e.status === 402) {
    // processor declined the first charge
  }
}

Runnable example

examples/l2e-flow.mjs runs the whole quickstart against a live platform, and examples/sdk-smoke.mjs exercises every read namespace at once:

npm run build
TAP2PAY_API_KEY=t2p_live_… node examples/l2e-flow.mjs
T2P_KEY=t2p_test_…          node examples/sdk-smoke.mjs

The full REST reference (device protocol, mobile enrollment, admin plane) lives at /docs on any platform instance.