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

@aynicobros/node

v0.2.0

Published

Server-side client for ayni cobros. Creates charges with your secret key and hands back the hosted-checkout link to redirect a payer to.

Readme

@aynicobros/node

Server-side client for ayni cobros: create a charge with your merchant secret key and get back the hosted-checkout link to redirect a payer to.

Where this fits

An ayni integration is two packages across a redirect:

  1. @aynicobros/node (this package, on your server) creates a charge with your secret key and gets back a checkoutUrl and a publicToken.
  2. You redirect the payer to checkoutUrl. Ayni hosts the payment page — QR, bank polling, "ya transferí" — and returns them to your successUrl with ayni_ref=<publicToken> appended.
  3. Your browser code reads the outcome with @aynicobros/js, which authenticates with that public token and needs no secret.

This package only does step 1. It has no DOM types, on purpose — a browser-only mistake fails to compile instead of failing in production.

Install

npm install @aynicobros/node

Requires Node.js 20 or later. Ships as ESM only (import, not require).

Quick start

import { createClient, datePlusDays } from '@aynicobros/node';

const client = createClient({
  apiKey: process.env.AYNI_SECRET_KEY!, // ayn_live_... or ayn_test_...
});

const result = await client.createCharge({
  reference: 'order-1042', // YOUR order id — also the idempotency key
  mode: 'live', // must agree with the key's own mode (see below)
  currency: 'BOB',
  amount: '250.00', // a decimal STRING — see "Amounts" below
  dueDate: datePlusDays(3),
  successUrl: 'https://shop.example.com/checkout/success',
  cancelUrl: 'https://shop.example.com/checkout/cancel',
});

switch (result.kind) {
  case 'ok':
    // Send the payer here.
    redirect(result.charge.checkoutUrl);
    break;
  case 'conflict':
    // `reference` already exists with DIFFERENT terms. Not a retry — this is
    // almost always a reused order id, or a replay that dropped `singleUse: false`.
    log.error('charge reference conflict', result.message);
    break;
  case 'unauthorized':
    // The key is missing, malformed, revoked, or not permitted this mode.
    log.error('ayni auth failed', result.message);
    break;
  case 'invalid':
    // The API (or this client, before the request was even sent) rejected the
    // terms. `message` names the field.
    log.error('invalid charge terms', result.message);
    break;
  case 'rate-limited':
    // Back off before retrying.
    break;
  case 'unavailable':
    // Network failure, timeout, or an unexpected status. The charge MAY have
    // been created — retry with the SAME reference, never a fresh one.
    log.warn('ayni unavailable, will retry by reference');
    break;
}

CreateChargeResult is a discriminated union, not an exception. Handle every kind — a handler that only reads result.charge on the happy path throws on every other branch, and on a payments path several of those branches are routine, not exceptional.

The secret key never leaves your server

apiKey is a merchant secret (ayn_live_… or ayn_test_…). Anything holding it can create charges in your name. It must never reach a browser bundle, a client-side environment variable, a log line, or a git commit. Read it from your own secret store — this package does not read process.env for you, so where the key comes from is your application's decision, not this library's.

This is also why Charge.payments (below) should never be forwarded wholesale to a browser: it carries the payer's bank identity.

API

createClient(config)

| Field | Type | Required | Notes | | ----------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | apiKey | string | yes | Merchant secret key. Throws TypeError if empty. | | apiOrigin | string | no | Defaults to ayni's API. Pass one only to override; an explicitly empty string throws rather than silently falling back. | | timeoutMs | number | no | Defaults to DEFAULT_TIMEOUT_MS (60 000 ms). Generous on purpose: creating a charge calls the bank, and the bank client itself retries up to three times at 15 s each plus backoff. |

Returns a client with one method, createCharge.

client.createCharge(input)

input (CreateChargeInput):

| Field | Type | Required | Notes | | -------------------------- | ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | reference | string | yes | Your order id, and the idempotency key. Letters, digits, and . _ : -, 1–64 characters. Replaying the same reference returns the original charge instead of minting a second QR — retry after a timeout by reusing it. | | mode | 'test' \| 'live' | yes | Must agree with the key's own mode. The API takes the actual mode from the key, not this field — this field exists so a mismatch is a loud, immediate rejection instead of a silent charge created in the wrong universe. | | currency | 'BOB' \| 'USD' | yes | | | amount | string | yes | A decimal string, e.g. "19.99" — never a number, and never minor units/cents. A JSON number round-trips through a double (19.99 can arrive as 19.989999999999998); a string keeps the amount you meant. See "Amounts" below. | | dueDate | string | yes | yyyy-MM-dd, as a Bolivian calendar day. Use currentDate() / datePlusDays() below rather than computing it yourself. | | singleUse | boolean | no | Defaults to true. false makes a reusable QR shared by many payers — and the type system then forbids payerName on it (a name on a shared QR would show every later payer the first one's name). | | payerName | string | no | Single-use charges only; see above. | | modifyAmount | boolean | no | Lets the payer change the amount in their banking app. Defaults to false. | | description | string | no | | | branchCode | string | no | The bank caps this at 5 characters. | | metadata | Record<string, unknown> | no | Echoed back untouched, never sent to the bank. | | successUrl / cancelUrl | string | no | Absolute http(s) URL. Ayni appends ayni_ref=<publicToken> when it returns the payer. | | accountId | string | no | Which account to credit. Omit to use the tenant's default for the given currency. |

Returns Promise<CreateChargeResult>:

| kind | Meaning | | ---------------- | ------------------------------------------------------------------------------------------------------------------- | | 'ok' | charge: Charge — see below. | | 'conflict' | reference exists with different terms. Not retryable as-is. | | 'unauthorized' | Key missing, malformed, revoked, or not permitted this mode. | | 'invalid' | Terms rejected; message names the field. | | 'rate-limited' | Back off. | | 'unavailable' | Network failure, timeout, or unexpected status. Retry with the same reference — the charge may already exist. |

Charge (the ok payload)

The full shape returned on success: id, reference, publicToken (safe to hand the payer — it is not id), checkoutUrl (redirect the payer here), status, amount, amountMinor, currency, singleUse, modifyAmount, dueDate, description, branchCode, providerChargeId, qrImageUrl, createdAt, settledAt, metadata, successUrl, cancelUrl, payerName, and payments.

payments is the one field to keep server-side. Each entry carries senderName, senderBankCode, and senderAccountMasked — the bank identity of whoever paid. Returning this array wholesale from your own API route publishes every payer's name and masked account to whoever loads the page. Pick the fields you actually need and forward those, not the object.

Amounts

amount on the way in (CreateChargeInput.amount) is a decimal string in major currency units — "250.00" means 250 BOB, not 25000 centavos. Sending minor units here overcharges the payer by 100×.

The Charge you get back also includes amountMinor, an integer in minor units (100 minor units = 1 major unit, for both BOB and USD) alongside the same amount as a decimal string. Only amountMinor is the API's internal integer representation; amount is what you sent.

Bolivia date helpers

import { currentDate, datePlusDays } from '@aynicobros/node';

currentDate(); // "2026-09-17", as read on a wall clock in La Paz right now
datePlusDays(3); // 3 calendar days from today, same rule

dueDate needs a Bolivian calendar day, and the obvious one-liner — new Date().toISOString().slice(0, 10) — is UTC. Bolivia is UTC-4 with no daylight saving, so between 20:00 and midnight local time that expression already reports tomorrow's date; a charge created then gets the wrong due date, and the mistake never throws, only shows up later as a charge expiring a day early or late. These two helpers exist so you don't have to get that right yourself.

Other exports

CURRENCIES (['BOB', 'USD']) and its Currency type, AYNI_API_ORIGIN, DEFAULT_TIMEOUT_MS, and the ClientConfig / AyniCobrosClient / CreateChargeInput / CreateChargeResult types.

Next step: reading the outcome

Once the payer lands back on your successUrl, read ayni_ref from the query string and pass it to @aynicobros/js's getCheckoutStatus — from your browser code, with no secret involved. See that package's README for the polling cadence to use while you wait for a status to settle.