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/js

v0.2.0

Published

Headless client for the ayni cobros hosted checkout. Reads a charge by its public token, and owns the polling decisions behind it.

Downloads

873

Readme

@aynicobros/js

Browser-safe client for reading an ayni cobros charge's status by its public token — plus the polling cadence and status predicates ayni's own checkout page uses, so you don't have to rediscover them.

Where this fits

An ayni integration is two packages across a redirect:

  1. @aynicobros/node (on the merchant's server) creates a charge with the merchant's secret key and gets back a checkoutUrl and a publicToken.
  2. The payer is sent to checkoutUrl. Ayni hosts the payment page — QR, bank polling, "ya transferí" — and returns them to the merchant's successUrl with ayni_ref=<publicToken> appended.
  3. This package, in the browser, reads the outcome with getCheckoutStatus and decides what to do next.

This package needs no secret. The credential is the token itself: a 128-bit capability the payer was already handed, good for the terms and status of that one charge and nothing else. If something here ever appears to want an API key, that would be the wrong design — creating a charge belongs on the server, in @aynicobros/node.

Install

npm install @aynicobros/js

Ships as ESM only. getCheckoutStatus uses fetch, AbortSignal.timeout, and AbortSignal.any — evergreen browsers, or a polyfill for AbortSignal.any on older ones.

Quick start

import { getCheckoutStatus, outcomeOf, isRetryable } from '@aynicobros/js';

const token = new URLSearchParams(location.search).get('ayni_ref');
if (!token) {
  throw new Error('missing ayni_ref on the return URL');
}

const result = await getCheckoutStatus(token);

switch (result.kind) {
  case 'ok': {
    const outcome = outcomeOf(result.checkout.status);
    if (outcome === 'paid') {
      // The only state in which you may release the order.
    } else if (outcome === 'awaiting') {
      // Not paid YET, and it still can be — hold the order. Do not cancel it.
    } else {
      // 'unpaid': nothing is owed and nothing will arrive. Safe to release
      // the reservation. If isRetryable(result.checkout.status), the SAME
      // order can be charged again (only true for REJECTED).
      if (isRetryable(result.checkout.status)) {
        // Offer to retry this order.
      }
    }
    break;
  }
  case 'not-found':
    // Unknown token, malformed token, or a charge whose QR was never minted —
    // all three answer identically, so this can't be used to enumerate tokens.
    break;
  case 'rate-limited':
    // Back off — see "Polling" below before you retry.
    break;
  case 'unavailable':
    // Network failure, timeout, or unexpected status. Retryable, never terminal.
    break;
}

getCheckoutStatus never throws and always settles — it does not hang on a dead connection, which is the ordinary outcome of a mobile-network handoff mid-request, not a corner case. CheckoutResult is a discriminated union; handle every kind. A handler that only reads result.checkout on the happy path breaks on the other three, and not-found in particular is a normal answer, not a bug.

Status: use the predicates, not your own switch

A charge's status is one of seven wire values (UNRESOLVED | PENDING | COLLECTING | PAID | EXPIRED | CANCELLED | REJECTED), and several of them are easy to misread: REJECTED looks final and is not; COLLECTING looks like an error and is a healthy reusable QR; and treating "not PAID" as "failed" tells a payer their money vanished while the bank is still settling. Use the exported predicates instead of writing your own:

| Function | Answers | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | isPaid(status) | Money confirmed. The only value this is true for is PAID — not a range. On a reusable QR, COLLECTING means somebody paid, never that your payer did. | | isPayable(status) | The payer can still complete this payment (PENDING or COLLECTING). | | isTerminal(status) | The charge will not change again (PAID, EXPIRED, or CANCELLED) — not the same question as success. An expired charge is terminal and unpaid. | | isRetryable(status) | The same order reference may be charged again. True only for REJECTED, where the bank created nothing. EXPIRED/CANCELLED need a new reference, or you risk two live codes for one order. | | outcomeOf(status) | Collapses all seven statuses into the three decisions that actually matter: 'paid', 'awaiting', or 'unpaid'. Prefer this over the four predicates above when what you want is "what do I do now". | | isChargeStatus(value) | Type guard — use before trusting a raw string as a ChargeStatus. | | isLive(mode) | Whether a ChargeMode ('TEST' \| 'LIVE') can move real money. |

outcomeOf is the one to reach for first: 'awaiting' covers PENDING, COLLECTING, and UNRESOLVED — the bank hasn't confirmed the QR even exists yet, which is not the same as unpaid, and nightly reconciliation is what eventually resolves it. Cancelling an order on 'awaiting' is the mistake this exists to prevent.

Checkout shape

The ok result carries a checkout: Checkout with status, mode ('TEST' | 'LIVE' — a TEST charge is unscannable by a real bank and collects nothing; shipping goods on one is shipping for free), amount (a decimal string, e.g. "19.99" — floats lose cents), currency, description, dueDate, modifyAmount, singleUse, qrImageUrl, settledAt, successUrl, cancelUrl, and three fields worth extra care:

  • singleUse — load-bearing for "did my payer pay". On a reusable QR the status is shared across every payer of that code, so no status alone can answer that question for one payer.
  • merchant / account — optional and nullable, even though the API always sends them today. There's no runtime validation on the response, and a store pins a version of this package and upgrades on its own schedule — an old bundle meeting a newer API projection is the normal state, not a bad deploy. Always optional-chain (checkout.merchant?.name), never assume presence.
  • payerName — the name the merchant set for the payer, single-use charges only. Never collected from the payer by this package.

Polling: pick the window for what's on screen

This package does not run a polling loop for you. It exports one read (getCheckoutStatus) and the constants below — the same cadence ayni's own hosted checkout page uses — so that whatever loop you write doesn't have to rediscover values that have already caused real incidents when set wrong. Every constant is a default: pass your own interval to the loop you build, don't hardcode these.

| Situation on screen | Constant | Value | Why | | ------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Nothing yet — a QR sitting there, no button pressed | BACKGROUND_POLL_MS | 30 s | Cheap for the server (an indexed read, never the bank), but not free for your rate-limit bucket. Give up after BACKGROUND_POLL_WINDOW_MS (10 min) and offer a manual retry instead of polling an abandoned tab forever. | | The payer just clicked "I already paid" | CONFIRM_POLL_MS / CONFIRM_WINDOW_MS | 5 s, for up to 60 s | This path calls the bank on ayni's side, so it's faster but time-boxed — a payment the bank hasn't reported yet isn't a payment that failed, so stop and show a "contact support" state rather than declaring failure. | | The page gives the payer their own manual control (a visible QR with a confirm button, or "Reintentar") | CONFIRMABLE_POLL_MS | 60 s | Slower, not off: the control beside it can ask the bank directly on demand, so the background read only has to catch a webhook or a merchant-side settlement. This is also the most common page in the product, so its cadence is what decides what a crowd behind one shared address costs you. | | You just got a 429 | BACKOFF_AFTER_429_MS | 30 s, as a floor | Take Math.max(BACKOFF_AFTER_429_MS, yourNormalInterval) — never this value on its own. Used as a flat replacement, it has sped up a 60 s poller to 30 s right after the bucket it just hit told it to slow down. |

Rate limits, and why the numbers above are what they are: API_CHECKOUT_READ_LIMIT_PER_MINUTE (30/min) applies to the plain status read above; API_CHECKOUT_REFRESH_LIMIT_PER_MINUTE (180/min) applies to the bank-backed confirm call, which is a separate, more expensive bucket. Both are sized against CLIENTS_PER_ADDRESS (10) — CGNAT means one public IP on a Bolivian mobile network is routinely ten different payers, not one, so a cadence that looks safe for a single browser can 429 an entire shared address. Polling faster than the table above risks exactly that.

CORS

Browser calls from this package are subject to a per-merchant allowlist held in ayni's own dashboard. Register your origin there first — until you do, requests are blocked before this code ever runs, and the SDK will look broken when the missing piece is configuration.

Other exports

AYNI_API_ORIGIN, DEFAULT_READ_TIMEOUT_MS (15 s — this is an indexed database read, never the bank, so anything slower is a lost connection, not a slow answer), CHARGE_STATUSES, CHARGE_MODES, and the ChargeStatus / ChargeMode / PaymentOutcome / Checkout / CheckoutMerchant / CheckoutAccount / CheckoutResult / GetCheckoutStatusOptions types.

Previous step: creating the charge

If you don't yet have a token to check, start with @aynicobros/node on your server — it creates the charge and hands you the checkoutUrl to send the payer to in the first place.