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

@cynco/billing

v0.3.0

Published

Accept payments with 3 API calls. Accounting happens automatically.

Readme

@cynco/billing

Accept payments with 3 API calls. Accounting happens automatically.

const pay = new CyncoBilling({ key: "cp_sk_..." });

// Subscribe a customer
await pay.subscribe({ customer: "user_123", product: "pro", successUrl: "/thanks" });

// Check if they can use a feature
const { allowed } = await pay.check("user_123", "api_calls");

// Track usage
await pay.track("user_123", "api_calls");

Every payment auto-posts to the general ledger. One integration replaces Stripe + QuickBooks.

No Stripe key needed. No CHIP key needed. Just cp_sk_... — we handle the payment gateway for you.

Install

npm install @cynco/billing

Quick Start (5 minutes)

1. Get your API key

Create a key in your Cynco dashboard or via the API:

curl -X POST https://app.cynco.io/api/v1/pay/api-keys \
  -H "Authorization: Bearer cp_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "Production", "type": "secret" }'

2. Create a product

import { CyncoBilling } from "@cynco/billing";

const pay = new CyncoBilling({ key: process.env.CYNCO_PAY_SECRET_KEY });

await pay.createProduct({
  name: "Pro",
  slug: "pro",
  prices: [{ type: "recurring", amount: 2000, billingInterval: "month" }],
  features: [
    { slug: "api_calls", name: "API Calls", type: "metered", allowanceType: "fixed", allowance: 10000 },
    { slug: "sso", name: "SSO", type: "boolean" },
  ],
});

3. Subscribe a customer

const result = await pay.subscribe({
  customer: { id: "user_123", email: "[email protected]", name: "Jane" },
  product: "pro",
  successUrl: "https://yourapp.com/billing?success=true",
  cancelUrl: "https://yourapp.com/billing",
});

if (result.url) {
  // Redirect to checkout
  redirect(result.url);
} else {
  // Activated immediately (stored card charged)
  console.log(result.subscription);
}

4. Gate features

const { allowed, balance } = await pay.check("user_123", "api_calls");

if (!allowed) {
  return new Response("Upgrade required", { status: 402 });
}

// Do the work, then track
await pay.track("user_123", "api_calls");

That's it. The customer is subscribed, gated, metered, and billed. GL entries posted automatically.


Core Concepts

Check + Track (two-call pattern)

check → allowed? → do work → track

check tells you if the customer has access. track records usage and decrements the balance. For high-concurrency scenarios, use the atomic one-call pattern:

// Atomic check + deduct in one call — zero race conditions
const { allowed } = await pay.check("user_123", "api_calls", { sendEvent: true });

Balance Locking (for AI completions)

When you don't know the final cost upfront:

// 1. Reserve tokens
const { allowed, lockId } = await pay.check("user_123", "ai_tokens", {
  requiredBalance: 4000,
  sendEvent: true,
  lock: { enabled: true, expiresAt: Date.now() + 60_000 },
});

// 2. Do the work
const completion = await openai.chat.completions.create({ ... });

// 3. Finalize with actual usage
await pay.finalizeLock({
  lockId,
  action: "confirm",
  overrideValue: completion.usage.total_tokens,
});

Unused tokens are refunded automatically. Locks expire if you don't finalize.

Subscriptions

// Subscribe (new, upgrade, or downgrade — handled automatically)
await pay.subscribe({ customer: "user_123", product: "pro", successUrl: "..." });

// Cancel at end of billing period
await pay.updateSubscription({ customer: "user_123", product: "pro", cancelAction: "cancel_end_of_cycle" });

// Uncancel
await pay.updateSubscription({ customer: "user_123", product: "pro", cancelAction: "uncancel" });

// Cancel immediately with prorated refund
await pay.updateSubscription({ customer: "user_123", product: "pro", cancelAction: "cancel_immediately" });

Upgrade & Downgrade

Upgrades charge a prorated amount immediately. Downgrades are scheduled at period end.

// Preview what the customer will pay
const preview = await pay.previewAttach("user_123", "enterprise");
console.log(preview.lineItems); // [{ title: "Enterprise", amount: 5000 }, { title: "Credit for Pro", amount: -1500 }]
console.log(preview.total);     // 3500

// Execute the upgrade
await pay.subscribe({ customer: "user_123", product: "enterprise" });

Carry-Over on Upgrade

// Carry unused balance from old plan
await pay.subscribe({
  customer: "user_123",
  product: "enterprise",
  carryOverBalances: { enabled: true },
});

// Or carry usage (deduct prior usage from new allowance)
await pay.subscribe({
  customer: "user_123",
  product: "enterprise",
  carryOverUsages: { enabled: true, featureIds: ["credits"] },
});

Pricing Models

Free Plan

await pay.createProduct({
  name: "Free",
  slug: "free",
  autoEnable: true, // auto-assigned on customer creation
  prices: [{ type: "recurring", amount: 0, billingInterval: "month" }],
  features: [
    { slug: "api_calls", name: "API Calls", type: "metered", allowanceType: "fixed", allowance: 100 },
  ],
});

Usage-Based Pricing

await pay.createProduct({
  name: "Pay As You Go",
  slug: "payg",
  features: [
    {
      slug: "notifications",
      name: "Notifications",
      type: "metered",
      allowanceType: "fixed",
      allowance: 1000, // included free
      overageAllowed: true,
      overagePrice: 1, // $0.01 per notification after included
    },
  ],
});
// Overage billed automatically at end of billing period

One-Off Purchase (Credit Top-Up)

await pay.subscribe({
  customer: "user_123",
  product: "credit_top_up",
  quantity: 500, // buy 500 credits
  successUrl: "...",
});

Per-Seat Pricing

// Create a seat entity
await pay.createEntity({ customer: "org_123", entityId: "user_alice", featureId: "seats", name: "Alice" });

// Check entity-level balance
const { allowed } = await pay.check("org_123", "ai_messages", { entityId: "user_alice" });

// Track entity-level usage
await pay.track("org_123", "ai_messages", { entityId: "user_alice", amount: 1 });

// Remove a seat (auto-decrements count)
await pay.deleteEntity("org_123", "user_alice");

Tiered Pricing

Graduated (each tier at its own rate) and volume (single rate by total) are both supported. Configure via product creation with tiers on the price.


Customers

// Idempotent get-or-create — safe to call on every login
const customer = await pay.getOrCreateCustomer({
  customerId: "user_123",
  name: "Jane Doe",
  email: "[email protected]",
});

// Returns: subscriptions, balances, flags, payment methods
console.log(customer.subscriptions);
console.log(customer.balances.api_calls.remaining);
console.log(customer.flags.sso); // boolean features as flags

// Update customer info
await pay.updateCustomer({ customerId: "user_123", name: "Jane Smith" });

// Grant promotional credits (standalone balance)
await pay.createBalance({ customer: "user_123", feature: "credits", grantedBalance: 500 });

Webhooks

import { verifyWebhook } from "@cynco/billing/webhooks";

app.post("/webhooks/cynco", (req, res) => {
  const event = verifyWebhook(req.body, req.headers["x-cynco-signature"], SECRET);

  switch (event.type) {
    case "subscription.activated":
      // Provision access
      break;
    case "payment.failed":
      // Send custom notification
      break;
    case "entitlement.exhausted":
      // Upsell prompt
      break;
  }

  res.json({ received: true });
});

Register a webhook endpoint:

const webhook = await pay.createWebhook({
  url: "https://yourapp.com/webhooks/cynco",
  events: ["subscription.activated", "payment.failed"],
});
// Save webhook.secret — shown only once

React

import { CyncoBillingProvider, useCyncoBilling, useBalance, useSubscriptions, useEntity } from "@cynco/billing/react";

function App() {
  return (
    <CyncoBillingProvider publishableKey="cp_pk_..." customerId="user_123">
      <Dashboard />
    </CyncoBillingProvider>
  );
}

function Dashboard() {
  const { check, subscribe, track } = useCyncoBilling();
  const { balance, granted, usage } = useBalance("api_calls");
  const { subscriptions } = useSubscriptions();

  return (
    <div>
      <p>{usage} / {granted} API calls used</p>
      <button onClick={() => subscribe("pro", { successUrl: "/thanks" })}>
        Upgrade to Pro
      </button>
    </div>
  );
}

function WorkspaceView({ entityId }: { entityId: string }) {
  const { check, track } = useEntity(entityId);
  // Entity-scoped operations
}

Billing Portal

Generate a self-service portal URL for customers to manage subscriptions and update payment methods:

const { url } = await pay.portal("user_123");
// Redirect customer to url — signed, 1-hour TTL

Analytics

// MRR, ARR, churn, ARPU, trial conversion
const metrics = await pay.analytics();

// Revenue timeline for charts
const timeline = await pay.revenueTimeline(12);

// Usage events over time (pass to Recharts)
const events = await pay.aggregateEvents("user_123", "api_calls", { range: "30d", groupBy: "day" });

Coupons

// Create a coupon
const coupon = await pay.createCoupon({
  code: "SAVE20",
  type: "percentage",     // "percentage" | "fixed" | "trial_extension"
  value: 20,              // 20% off
  duration: "repeating",  // "once" | "repeating" | "forever"
  durationMonths: 3,      // applies for 3 billing cycles
  maxRedemptions: 100,
});

// Validate at checkout (safe with publishable key)
const { valid, discountAmount, finalAmount } = await pay.validateCoupon({
  code: "SAVE20",
  customer: "user_123",
  product: "pro",
  amount: 2000,
});
// → { valid: true, discountAmount: 400, finalAmount: 1600 }

// Update or archive
await pay.updateCoupon("pcpn_1", { name: "Summer Sale" });
await pay.archiveCoupon("pcpn_1");

API Keys

// Create (raw key shown only once — store securely)
const { rawKey } = await pay.createApiKey({ name: "Production", type: "secret" });

// List (masked) and revoke
const keys = await pay.listApiKeys();
await pay.revokeApiKey("pak_1");

Idempotency

All billing-mutating endpoints support safe retries via the Idempotency-Key header:

await pay.subscribe(
  { customer: "user_123", product: "pro", successUrl: "..." },
  { idempotencyKey: "checkout_abc123" },
);

await pay.cancel("user_123", { product: "pro" }, { idempotencyKey: "cancel_abc123" });

Product Versioning

When you update a product, existing subscribers stay grandfathered on their version:

// List versions
const { versions } = await pay.listProductVersions("pprod_xxx");

// Migrate customers to latest
await pay.migrateCustomers("pprod_xxx", "pver_old_version_id");

Error Handling

import { CyncoBillingError } from "@cynco/billing";

try {
  await pay.subscribe({ ... });
} catch (err) {
  if (err instanceof CyncoBillingError) {
    console.log(err.code);    // "VALIDATION_ERROR"
    console.log(err.status);  // 422
    console.log(err.details); // [{ field: "customer", message: "required" }]
  }
}

Required Actions

When a payment can't be processed automatically (3DS, card declined):

const result = await pay.subscribe({ ... });

if (result.requiredAction) {
  console.log(result.requiredAction.code);   // "payment_failed" | "3ds_required"
  console.log(result.requiredAction.reason); // "Card was declined"
  // Redirect to result.url for the customer to resolve
}

API Reference

Core

| Method | Description | |--------|-------------| | subscribe(input) | Subscribe, upgrade, or downgrade a customer | | check(customer, feature, options?) | Check feature access (with optional atomic deduct or lock) | | track(customer, feature, options?) | Record usage for a metered feature | | cancel(customer, options?) | Cancel a subscription | | finalizeLock(input) | Confirm, release, or adjust a balance lock |

Customers

| Method | Description | |--------|-------------| | getOrCreateCustomer(input) | Idempotent get-or-create | | getCustomer(id) | Get customer with subscriptions, balances, flags | | updateCustomer(input) | Update name/email | | deleteCustomer(id) | Delete customer mapping | | listCustomers() | List all customers |

Products & Plans

| Method | Description | |--------|-------------| | createProduct(input) | Create product with prices and features | | listProducts() | List all products | | listPlans(customer?) | List plans with eligibility | | previewAttach(customer, product) | Preview charge before subscribing |

Subscriptions

| Method | Description | |--------|-------------| | updateSubscription(input, options?) | Cancel, uncancel, pause, resume, or change quantities | | previewUpdate(input) | Preview subscription changes | | listSubscriptions(status?, params?) | List subscriptions |

Balances & Entities

| Method | Description | |--------|-------------| | createBalance(input) | Grant standalone credits | | updateBalance(input) | Set usage or balance directly | | createEntity(input) | Create entity (seat/workspace) | | deleteEntity(customer, entityId) | Remove entity | | listEntities(customer, params?) | List entities |

Billing

| Method | Description | |--------|-------------| | portal(customer) | Generate billing portal URL | | analytics() | MRR, ARR, churn, ARPU | | revenueTimeline(months?) | Monthly revenue chart data | | aggregateEvents(customer, feature, options?) | Usage timeline | | getPricingTable(customer?) | Embeddable pricing table with formatted prices |

Webhooks

| Method | Description | |--------|-------------| | createWebhook(input) | Register endpoint | | listWebhooks(params?) | List endpoints | | deleteWebhook(id) | Remove endpoint |

Coupons

| Method | Description | |--------|-------------| | createCoupon(input) | Create a discount coupon | | listCoupons(params?) | List all coupons | | getCoupon(id) | Get coupon by ID | | updateCoupon(id, input) | Update coupon properties | | archiveCoupon(id) | Soft-delete a coupon | | validateCoupon(input) | Validate a code for a customer + product |

Spend Caps

| Method | Description | |--------|-------------| | getSpendCap(customer, feature) | Get spend cap status | | setSpendCap(input) | Set or remove a customer spend cap | | setProductSpendCap(input) | Set default cap for a product |

API Keys

| Method | Description | |--------|-------------| | createApiKey(input) | Create a new API key (raw key shown once) | | listApiKeys(params?) | List API keys (masked) | | revokeApiKey(id) | Revoke an API key |

Rewards & Referrals

| Method | Description | |--------|-------------| | createRewardProgram(input) | Create a referral/reward program | | listRewardPrograms() | List all programs | | createReferralCode(input) | Create a referral code for a customer | | listReferralCodes(customer) | List referral codes | | redeemReferralCode(input) | Redeem a referral code |

Audit & Versioning

| Method | Description | |--------|-------------| | listAuditEvents(filters?) | List billing audit trail | | listProductVersions(productId) | List version history | | migrateCustomers(productId, versionId) | Migrate to latest |


What Makes Cynco Billing Different

  • Accounting built in. Every charge auto-posts to the general ledger (DR Receivable 1200, CR Revenue 4001). No reconciliation needed.
  • Balance locking. Reserve tokens before an AI completion, finalize with actual usage. Purpose-built for AI SaaS.
  • CHIP + Stripe. Malaysian payment gateway native. Not just a Stripe wrapper.
  • Zero infrastructure. No Redis, no queues, no external services. Postgres advisory locks for concurrency. All-in-one.
  • Per-customer locking. Billing operations are serialized per customer. No double-charges from race conditions.
  • API idempotency. Idempotency-Key header on all billing endpoints. Safe retries.

License

MIT