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

@payitgo/node

v0.9.0

Published

Node.js library for the payitgo crypto payment gateway

Readme

payitgo for Node.js

Accept crypto payments, and pay your own clients, with payitgo from Node.js 18+. TypeScript types included, no runtime dependencies.

Full reference, with every method, option, type and error: api.payitgo.co/docs/sdk-node.

npm install @payitgo/node
import Payitgo from "@payitgo/node";              // ES modules and TypeScript; also { Payitgo, PayitgoError, … }
const { Payitgo } = require("@payitgo/node");     // CommonJS

Accept a payment in three steps

import express from "express";
import { Payitgo } from "@payitgo/node";

const payitgo = new Payitgo(process.env.PAYITGO_KEY!, {
  webhookSecret: process.env.PAYITGO_WEBHOOK_SECRET, // the signing secret: see Webhooks (IPN) below
});
const app = express();

// 1. Create a payment and send the customer to the checkout page.
app.post("/checkout", express.json(), async (req, res) => {
  const payment = await payitgo.payments.create({
    order_id: req.body.orderId,      // your order id: safe to retry
    amount: req.body.total,          // e.g. 19.99, in US dollars unless you give a currency
    // token: "USDT_TRC20",          // optional: without it the customer picks the coin
    success_url: "https://yourdomain.com/thanks",
  });
  res.redirect(payment.checkout_url!);
});

// 2. Receive the notification (the complete handler is under Webhooks (IPN) below).
//    Use the raw body: the signature covers its exact bytes.
app.post("/payitgo/ipn", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = payitgo.webhooks.constructEvent(req.body, req.get("X-Webhook-Signature"));
  } catch {
    return res.sendStatus(400);
  }
  // 3. Fulfil the order once the payment is final.
  if (event.type === "payment.confirmed") {
    markOrderPaid(event.data.order_id); // make this idempotent: events can arrive twice
  }
  res.sendStatus(200);
});

Start with a test key, where you simulate what the customer pays; switch to a live key when the account owner has turned live mode on. A key does not say its mode (the dashboard shows a Test or Live badge next to it): await payitgo.isLive() asks payitgo, once.

Prices in your own currency

A payment is priced in US dollars unless you give currency, an ISO 4217 code such as EUR, GBP or NGN (any case). Every figure on the payment is then in that currency: amount (your price), amount_due (what the customer pays: the price, plus the fees when you pass them on), amount_received (what has arrived so far), each deposit's value, each coin's minimum and network_fee in token_options, and rate_locked (the coin's price). Only crypto_amount and crypto_received are in the coin.

const payment = await payitgo.payments.create({
  order_id: "ORD-1002",
  amount: "45.50",     // a string avoids float rounding; a number works too
  currency: "EUR",     // any case; US dollars when left out
});
// payment.currency "EUR", payment.amount "45.5", payment.amount_due "45.5"

payitgo converts at the rate of the moment. When no exchange rate can be had, create rejects with a 503 (exchange rate unavailable for currency: try again shortly) once the client's retries are spent: try again shortly. A payment link priced in a coin has that coin as its currency ("ETH"), and its figures in the coin.

Your own payment page

checkout_url is only for payitgo's hosted page. To show the payment on your own page, render the payment your server gets back (deposit_address, crypto_amount, destination_tag for XRP, a QR code of qr_payload, expires_at); no checkout calls are needed.

  • You know the coin: create the payment with token and render the payment it returns.
  • The customer chooses it: create the payment without token, show payment.token_options on your page, then call payments.selectToken(payment.id, chosen) from your server and render the payment it returns: it now has its coin, address, amount, locked rate and a deadline 15 minutes away.
const payment = await payitgo.payments.create({ order_id: "ORD-1", amount: 10 });
// your page shows payment.token_options; the customer picks one
const chosen = await payitgo.payments.selectToken(payment.id, "USDT_TRC20"); // or your order id
// render chosen.deposit_address, chosen.crypto_amount, chosen.qr_payload, chosen.expires_at

The customer can change the coin while the payment still has token_options (nothing has arrived yet): call payments.selectToken again with another. Once it can no longer change, it is a 409 (payments.retrieve then returns the payment with its coin). A coin outside token_options, or a payment that can no longer be paid, is a 400.

Test mode

A test payment gets an address like a live one, but no coins are involved: you choose an outcome and payitgo plays its deposits, status changes and webhooks a few seconds apart (Test mode).

// Choose the outcome when you create the payment (with token it starts at once,
// without it once the coin is chosen)…
await payitgo.payments.create({ order_id: "TEST-1", amount: 10, token: "USDT_TRC20", simulate: "confirmed" });

// …or later, on a payment you already have (its id or your order id).
const payment = await payitgo.payments.simulate("TEST-2", "partially_paid");
// payment.simulation: { outcome: "partially_paid", state: "running" }

Outcomes: confirmed, overpaid, partially_paid, underpaid, expired, failed, wrong_asset, paid_late, paid_twice. Simulated deposits have simulated: true; their tx_id names no transaction on any chain. A live key gets 400; a payment whose simulation is still running, 409.

Webhooks (IPN)

payitgo POSTs a signed notification for every change of a payment (payment.confirmed, payment.expired, …), of a direct settlement and of a payout. Each mode has one webhook: a test key manages the test one, a live key the live one.

Where the notifications go. Set the URL once per mode (https:// on a public host, accepting POST requests), or give a payment its own when you create it:

// Once per mode (safe to repeat):
await payitgo.webhooks.set("https://api.yourdomain.com/payitgo/ipn");

// Or per payment: its events go there instead of the webhook's URL.
await payitgo.payments.create({ order_id: "ORD-1", amount: 10, webhook_url: "https://api.yourdomain.com/orders/ipn" });

// Or as the client's default for every payment it creates (a payment's own webhook_url wins):
const shop = new Payitgo(process.env.PAYITGO_KEY!, { webhookUrl: "https://api.yourdomain.com/orders/ipn" });

A storefront's key cannot set the account's URL (403): pass webhook_url instead. Events of your own payouts go to a payment's own webhook_url when it has one, else to its batch's webhook_url, else to the webhook's URL (withdrawals and schedules always go there).

The signing secret. Every notification of a mode is signed with that mode's secret, whichever URL it goes to. Copy it from the dashboard or read it with (await payitgo.webhooks.get()).secret, and give it to the client once:

const payitgo = new Payitgo(process.env.PAYITGO_KEY!, { webhookSecret: process.env.PAYITGO_WEBHOOK_SECRET });

Receiving them (Express). Verify the raw body, handle each event once, and answer 2xx within 10 seconds; otherwise payitgo retries after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h.

app.post("/payitgo/ipn", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    // Checks the signature with the client's webhookSecret and refuses anything older than 5 minutes.
    event = payitgo.webhooks.constructEvent(req.body, req.get("X-Webhook-Signature"));
  } catch {
    return res.sendStatus(400);
  }
  // The same event can arrive more than once: handle each event id once,
  // e.g. by inserting it into a table with a unique key.
  if (!(await recordEventOnce(event.id))) return res.sendStatus(200);

  switch (event.type) {
    case "payment.confirmed":
      await jobs.enqueue("fulfil", { orderId: event.data.order_id }); // slow work off the request
      break;
    case "payment.expired":
      await jobs.enqueue("release-stock", { orderId: event.data.order_id });
      break;
  }
  res.sendStatus(200);
});

Testing it. await payitgo.webhooks.test() queues a signed ping event to the webhook's URL and returns its delivery (400 when no URL is set).

Payouts

Pay your own clients (withdrawals from your platform, prizes, payroll) from your payout wallet: one address per network (Tron, BNB Smart Chain, Ethereum, Bitcoin, Litecoin, Dogecoin, XRP Ledger), yours alone, that you top up. Payouts are paid in USDT_TRC20, TRX, USDT_BEP20, USDC_BEP20, ETH_BEP20, BNB, USDT_ERC20, USDC_ERC20, DAI_ERC20, ETH, BTC, LTC, DOGE and XRP (with tag for XRP's destination tag), or in another coin, converted on its way (below) (Mass payouts).

Reading works with any key; everything else needs a key with payouts turned on in the dashboard (403 otherwise). The key's mode decides the wallet: a test key pays from a test wallet, on paper. Amounts are decimal strings; amounts you send may be strings or numbers.

Payout requests, one by one. Send each of your clients' payout requests as it comes in. payitgo checks and prices it at once and keeps it in your open list with its costs, until you send the list (or it goes by itself, with auto-send turned on in the dashboard):

// A client asks to withdraw. Pass your own id for the request as reference: sending it
// again (a retry, yours or the client's) returns the same payment instead of a second one.
const item = await payitgo.payouts.request({
  reference: "wd-1042",
  asset: "USDT_TRC20",
  address: "TR7mNkucnhLZoJQLLJ6DR88jpLQufWxUJi",
  amount: "120",
  name: "Ana Silva",             // optional, with email and note
});
if (item.status === "invalid") console.log(item.errors.map((e) => e.message)); // kept, never sent
else console.log(item.fee, item.send_amount, item.debit); // its costs, in the coin

// The open list, and what sending it needs of each coin against what the wallet can spend.
const { inbox, coverage, items } = await payitgo.payouts.inbox(); // inbox.totals, inbox.counts
for (const c of coverage) {
  if (Number(c.top_up) > 0) console.log(`send ${c.top_up} ${c.asset} to ${c.address} first`);
}

// Send every valid request, or some (payouts.send([id, …])): they become a batch, priced
// again and submitted. It then waits for approval and/or funds, or starts.
const batch = await payitgo.payouts.send();

// Out of the list (or canceled, if not on its way yet); safe to retry. Several at once:
// payouts.remove([id, …]), a result per id.
await payitgo.payouts.cancel(item.id);
await payitgo.payouts.retrieveByReference("wd-1042"); // or payouts.retrieve(item.id)

A request's own webhook_url (https) gets its events instead of the webhook's URL, and it keeps it when it is sent into a batch.

payouts.quote prices up to 100 payments without keeping anything, for example to show a client the fee before they confirm:

const { quote, items } = await payitgo.payouts.quote({
  fee_paid_by: "recipient",      // the fees come out of the amount ("merchant": on top)
  items: [{ asset: "USDT_BEP20", address: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", amount: "120" }],
});
// items[0].send_amount: what the client receives; quote.total_usd: what it takes from the wallet

Batches. A whole list at once (payroll, prizes): up to 1000 payments a call, 10,000 a batch.

const batch = await payitgo.payoutBatches.create({
  reference: "payroll-2026-10",  // safe to retry: a reference used already returns that batch
  name: "October payroll",
  items: [
    { reference: "payroll-2026-10-ana", asset: "USDT_TRC20", address: "TR7mNkucnhLZoJQLLJ6DR88jpLQufWxUJi", amount: "1500" },
    { reference: "payroll-2026-10-ben", asset: "USDT_BEP20", address: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", amount: "250" },
  ],
});
// "quoted": batch.totals (per coin) and batch.total_usd. Invalid rows are left out:
const invalid = await payitgo.payoutBatches.items(batch.id, { status: "invalid" }); // their errors say why

const submitted = await payitgo.payoutBatches.submit(batch.id);
// "pending_approval" (your payout settings ask for approvals, given in the dashboard),
// "awaiting_funds" (submitted.waiting.shortfall: what to send where), or "processing".

A batch's prices hold for an hour (15 minutes with converted payments): past it, submit is a 409 (its prices expired: get a new quote first): call payoutBatches.quote(batch.id), then submit. Also payoutBatches.addItems(id, items), list({ status }), retrieve(id) and cancel(id).

Claim links. A payment whose recipient chooses where to receive it: pass claim: true (or claim_assets, the other coins they may choose, paid at the same dollar value) and no address. With an email, payitgo emails them the link and they confirm with a code sent there; without one, you share the link (claim.url) yourself, and its PIN (claim.pin, 6 digits) another way, such as a call or a text message: the recipient enters it to claim, so the link alone pays no one. An email alone is not a claim link: a payment with no address and no claim is invalid. It works in payout requests, batches and quotes alike.

const item = await payitgo.payouts.request({
  reference: "prize-cup-1",
  asset: "USDT_TRC20",
  amount: "120",
  claim: true,
  email: "[email protected]",          // optional: emailed the link (else share item.claim.url and item.claim.pin)
  claim_assets: ["USDT_BEP20", "BTC"],  // optional: other coins they may choose
});

Once funded, the payment is claimable until its recipient claims it (payout_item.claimed: it is then queued, with the address and the coin they chose; claim.funded_asset and claim.funded_amount say what it was funded with when they chose another) or until claim.expires_at (payout_item.claim_expired: it is expired, its money free again in the wallet, its reference in released_reference). Read item.claim: url, pin, email, assets, expires_at, claimed_at, emailed, extended. Resending and extending a link are done in the dashboard. A bad email is an invalid_email error.

Converted payments. Pay a recipient in a coin your wallet does not hold: with convert: "convert", every payment in another coin than convert_from is paid from convert_from, converted on its way. Both default to your payout settings.

const batch = await payitgo.payoutBatches.create({
  reference: "wd-batch-7",
  convert: "convert",
  convert_from: "USDT_TRC20",           // the wallet's coin that pays
  items: [{ reference: "wd-2001", asset: "BTC", address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", amount: "0.002" }],
});
const { items } = await payitgo.payoutBatches.items(batch.id);
// items[0].convert: { to: "BTC", ticker, network, to_amount: "0.002", estimate: "0.00200041" }
// items[0].asset, amount, fee (converting included) and debit are in USDT_TRC20.

convert.estimate is about what arrives; convert.amount_out is what did, once sent. A coin that cannot be converted right now is a conversion_unavailable error. payouts.quote takes convert and convert_from too; payout requests follow your payout settings. Claim links are never converted.

The wallet.

const wallets = await payitgo.payoutWallets.list();
// [{ chain: "tron", network, address, assets: [{ asset, balance, reserved, spendable, needed }] }, { chain: "bsc", … }, …]

Send each network's address only its payout coins. needed is what batches waiting for funds lack: they start by themselves once it arrives.

Test mode. With a test key, top the test wallet up on paper. Payments are then sent on paper within seconds, with a made-up tx_id, and their webhooks arrive as live ones do:

await payitgo.payoutWallets.simulateTopUp("USDT_TRC20", "1000"); // test keys only: 400 with a live key

Webhooks. Each step raises an event, signed like the others: payout_batch.quoted, payout_batch.pending_approval, payout_batch.approved, payout_batch.rejected, payout_batch.awaiting_funds, payout_batch.processing, payout_batch.completed and payout_batch.canceled carry the batch; payout_item.processing (a request left your list for a batch), payout_item.returned (it came back: not final), payout_item.sent, payout_item.failed, payout_item.canceled (cancel_reason and canceled_by say why and who), payout_item.claimed and payout_item.claim_expired the payment; payout_wallet.withdrawn and payout_wallet.withdrawal_failed (fail_reason) the withdrawal. Schedules (made in the dashboard) send payout_schedule.run (a summary of the schedule, without its rows: last_batch_id is the batch it made, last_error why it made none) and payout_schedule.funding_needed ({ schedule, shortfall }, a few days before a run the wallet cannot cover); their batches have source: "schedule". A payment never sent (failed, canceled or expired) gives its reference back: it is then in released_reference, so the same request can be paid again.

Staying in sync. Every payment ends sent, failed, canceled or expired, with one event each, delivered at least once: deduplicate on the event's id (it never changes), and act on data.status and the latest data.updated_at, since order is not guaranteed. Replay what you missed with events.list({ after }) (the last event id you handled), and resync the payments with payouts.list({ cursor }) (keep its next_cursor).

import type { PayoutItem } from "@payitgo/node";

const event = payitgo.webhooks.constructEvent<PayoutItem>(req.body, req.get("X-Webhook-Signature"));
switch (event.type) {
  case "payout_item.sent":   // on chain
    await markWithdrawalPaid(event.data.reference, event.data.tx_id);
    break;
  case "payout_item.failed": // given up: its money stays in the wallet
    await reopenWithdrawal(event.data.released_reference, event.data.fail_reason);
    break;
}

API

const payitgo = new Payitgo(apiKey, {
  timeoutMs: 30_000,
  maxRetries: 2,                      // network errors, 429 and 5xx
  webhookSecret: process.env.PAYITGO_WEBHOOK_SECRET, // for webhooks.constructEvent
  webhookUrl: "https://…",            // default webhook_url of payments.create
});

await payitgo.isLive();                               // false for a test key (asks payitgo once)
await payitgo.payments.create({ order_id, amount, currency?, token?, tokens?, description?, metadata?,
                              customer_email?, success_url?, cancel_url?, webhook_url?, simulate? });
await payitgo.payments.selectToken(paymentId, token); // or your order id: the payment with its coin
await payitgo.payments.simulate(paymentId, "confirmed"); // test keys: plays the outcome
await payitgo.payments.retrieve(paymentId);           // or your order id
await payitgo.payments.list({ status: "confirmed", store_id: storeId, limit: 50, offset: 0 }); // every filter optional
await payitgo.tokens.list();                          // [{ token, chain, minimum, network_fee, fee_note }], in US dollars
await payitgo.tokens.list({ amount: "49.90", currency: "EUR" }); // the coins a 49.90 EUR payment offers, with amount_due
await payitgo.checkout.retrieve(checkoutToken);       // public payer view
await payitgo.checkout.selectToken(checkoutToken, token); // public: the customer's choice of coin
await payitgo.payouts.request({ reference, asset, address, amount, tag?, name?, email?, note?, claim?, claim_assets?, webhook_url? }); // PayoutItem
await payitgo.payouts.retrieve(itemId);               // PayoutItem: a request or a batch's row
await payitgo.payouts.retrieveByReference(reference); // PayoutItem: the one carrying it, or the latest that gave it back
await payitgo.payouts.list({ status?, updated_since?, cursor?, limit? }); // { items, has_more, next_cursor }: oldest change first
await payitgo.payouts.inbox({ status?, limit?, offset? }); // PayoutInbox { inbox, coverage, items, total, limit, offset }
await payitgo.payouts.send(ids?);                     // PayoutBatch: every valid request, or those of ids
await payitgo.payouts.cancel(itemId);                 // PayoutItem, status "canceled"
await payitgo.payouts.remove(itemIds);                // [{ id, item } | { id, error: { status, message } }]
await payitgo.payouts.quote({ fee_paid_by?, convert?, convert_from?, items }); // { quote, items }: nothing kept
await payitgo.payoutBatches.create({ reference?, name?, fee_paid_by?, convert?, convert_from?, webhook_url?, items }); // PayoutBatch
await payitgo.payoutBatches.list({ status?, limit?, offset? }); // { batches, total, limit, offset }
await payitgo.payoutBatches.retrieve(batchId);        // PayoutBatch
await payitgo.payoutBatches.items(batchId, { status?, limit?, offset? }); // { items, total, limit, offset }
await payitgo.payoutBatches.addItems(batchId, items); // PayoutBatch
await payitgo.payoutBatches.quote(batchId);           // PayoutBatch, priced again
await payitgo.payoutBatches.submit(batchId);          // PayoutBatch
await payitgo.payoutBatches.cancel(batchId);          // PayoutBatch
await payitgo.payoutWallets.list();                   // PayoutWallet[]
await payitgo.payoutWallets.simulateTopUp(asset, amount); // PayoutWallet[]: test keys only
await payitgo.webhooks.get();                         // Webhook { livemode, url, secret, created_at, updated_at }
await payitgo.webhooks.set(url);                      // Webhook
await payitgo.webhooks.remove();                      // Webhook, url null (the secret stays)
await payitgo.webhooks.test();                        // WebhookDelivery of a "ping" event
await payitgo.events.list({ type?, after?, created_after?, limit? }); // { events, has_more }: oldest first, to replay
payitgo.webhooks.constructEvent(rawBody, signatureHeader, secret?, toleranceSeconds = 300);
Payitgo.webhooks.constructEvent(rawBody, signatureHeader, secret, toleranceSeconds = 300); // no client needed

The client calls payitgo's API, default https://api.payitgo.co; the baseUrl option (or the PAYITGO_BASE_URL environment variable) changes it. Every method returns the API's JSON with its field names (order_id, checkout_url, …); see the payment object and webhooks.

Errors

import { PayitgoError, SignatureVerificationError } from "@payitgo/node";

try {
  await payitgo.payments.create({ order_id: "ORD-1", amount: 1, token: "BTC" });
} catch (e) {
  if (e instanceof PayitgoError) console.log(e.status, e.message); // 400 "minimum payment in BTC is $10.00 right now (network fees)"
}

PayitgoError.status is the HTTP status (0 for a network error). constructEvent throws SignatureVerificationError for a bad signature, a stale or malformed timestamp, or when it has no secret (none passed and no webhookSecret). A timeout counts as a network error whether it happens before the answer starts or while it is read.

Development

npm install && npm test