← Back to site
Official SDK · npm install paygate-to

Node.js SDK

Card and crypto payments in Node.js with one npm package. The SDK handles wallets and payment links, prices and QR codes. It also verifies the signed callback and decodes the payout amounts exactly. It ships TypeScript types, ESM and CommonJS builds, and has no dependencies.

Install

Node.js 18 or newer. TypeScript types included, ESM and CommonJS, no dependencies.

npm install paygate-to
import { PayGate } from 'paygate-to';     // or: const { PayGate } = require('paygate-to');

const paygate = new PayGate();

// Production: your free dedicated endpoint and, optionally, your white-label checkout domain
const paygate = new PayGate({
  apiDomain: 'api-yourshop-1a2b3c.paygate.to',
  checkoutDomain: 'pay.yourshop.com',
});

Get your dedicated endpoint from the custom domain guide. Source code: github.com/paygate-to/paygate-node.

Card payments

There are three steps for each order. First, create a wallet tied to the order's callback URL. Then send the customer to the payment link. Finally, credit the order when the signed callback arrives. Payouts arrive in USDC on Polygon.

import { CallbackToken } from 'paygate-to';

const tokens = new CallbackToken(process.env.PAYGATE_CALLBACK_SECRET);   // 64 random hex chars, kept on your server

// The callback carries the order id and a token so the id cannot be swapped.
const callback = tokens.callbackUrl('https://shop.example/paygate/callback', { order: order.id });

const wallet = await paygate.card.createWallet('0xYourPolygonUsdcWallet', callback);

// What you expect to receive, in USD: the callback compares the amount that arrived with this.
order.expectedUsd = (await paygate.card.convertToUsd(order.total, 'EUR')).value;

res.redirect(paygate.card.paymentUrl(wallet, order.total, order.email, 'EUR'));

paymentUrl() opens the hosted page, which lists the providers available for the customer's country, amount and currency. Pass { provider: 'wert' } to send the customer straight to one provider. providers() lists the providers with their status and minimum order:

for (const provider of await paygate.card.providers()) {
  if (provider.active) console.log(provider.id, provider.minimumAmount, provider.minimumCurrency);
}

paygate.card.paymentUrl(wallet, '49.90', '[email protected]', 'USD', { provider: 'wert' });

The same callback URL always returns the same wallet, so rebuilding the link for an order is safe.

Crypto on your page

Show the address, the amount and a QR code on your own checkout page:

const wallet = await paygate.crypto.createWallet('btc', 'bc1qYourBitcoinWallet', callback);
const quote = await paygate.crypto.convert('btc', order.total, 'USD');   // { value: '0.00041512', exchangeRate: ... }
const qr = await paygate.crypto.qrCode('btc', wallet.addressIn, quote.value);

// Show these on your checkout page (escape them like any other value you put in HTML)
res.render('pay', { address: wallet.addressIn, amount: quote.value, qr: qr.dataUri });

Coins are written as btc, erc20_usdc or erc20/usdc. Use a payout wallet on the coin's network, for example an EVM address for ERC20, BEP20 and Polygon tokens. paygate.crypto.coins() lists every coin with its logo and minimum.

await paygate.crypto.createWallet('ltc', myLtcWallet, callback, {
  pending: true,       // also send a callback (pending=1) when the transaction is first seen
  confirmations: 3,    // confirmations required before the confirmed callback
});

Hosted multi-coin page

The customer picks the coin on the hosted page. Give one wallet per network you accept: evm, btc, bitcoincash, ltc, doge, solana, trc20, xmr, zec, ton.

const hosted = await paygate.crypto.createHostedWallet({
  btc: 'bc1qYourBitcoinWallet',
  evm: '0xYourEvmWallet',        // ETH, USDT, USDC... on ERC20, BEP20, Polygon, Arbitrum, Base...
  xmr: 'YourMoneroWallet',
}, order.total, 'USD', callback);

res.redirect(paygate.crypto.hostedPaymentUrl(hosted, {
  addFees: true,                   // the customer pays the network fee
  branding: { theme: '#7c3aed' },  // hex codes or CSS colour names
}));

Handling the callback

When a payment arrives, PayGate.to requests your callback URL with your parameters plus the payment data, and signs the request. Verify the signature first and fail closed:

import { CallbackData, coinId } from 'paygate-to';

const CALLBACK_URL = 'https://shop.example/paygate/callback';   // as registered

app.get('/paygate/callback', async (req, res) => {
  const result = await paygate.callbacks.verifyRequest(CALLBACK_URL, req);
  if (!result.valid) return res.sendStatus(403);                // result.reason: signature_mismatch...

  const data = CallbackData.fromRequest(req);
  if (!tokens.verify(data.get('order'), data.get('token'))) return res.sendStatus(403);

  if (data.pending) return res.send('ok');                      // seen, not yet confirmed: do not ship

  const order = await findOrder(data.get('order'));
  if (order.hasTransaction(data.txidIn)) return res.send('ok'); // callbacks can repeat: credit each payment once
  await order.addPayment(data.txidIn, data.coin, data.valueCoin);

  // Card payments arrive in USDC after the provider's fee: the official plugins accept 60% of the USD total or more.
  const paid = order.method === 'card'
    ? order.receivedTotal() >= 0.6 * Number(order.expectedUsd)
    : coinId(data.coin) === coinId(order.coin) && order.receivedTotal() >= 0.98 * Number(order.expectedCoin);
  await order.setStatus(paid ? 'paid' : 'underpaid');
  res.send('ok');
});

Each check matters:

  • Signature: proves PayGate.to sent the request and that nothing in it was changed.
  • Token: the callback URL is chosen when the wallet is created, so anyone can create a wallet whose callback names one of your orders and pay it a few cents. The token proves your server issued that order reference. Without tokens, compare the callback's address_in with the order's deposit address (the Polygon address for cards); the hosted page has no address in advance, so use tokens there.
  • Transaction id: callbacks are repeated until you answer ok.
  • Coin and amount: the payment link is visible to the customer, and any token can be sent to a crypto address.
  • Payout address: callback URLs end up in web server logs. With a leaked URL, someone could create a wallet that pays them and still call your callback. The payout helper returns what reached your wallet; expect it to be at least 90% of the amount forwarded.

Never use PayGate.to's parameter names (value_coin, coin, txid_in, txid_out, address_in, value_forwarded_coin, address_out, pending) in your callback URL; the SDK refuses them. If a name appears twice, the SDK uses the last value, which is PayGate.to's.

Pass verifyRequest() the callback URL you registered, written as a constant in your code. The signed string is that URL's scheme, host and path followed by the query string exactly as it arrived. The SDK reads it from req.originalUrl (Express) or req.url (Node's http) and never rebuilds it. Rebuilding the query from req.query or URLSearchParams would break the signature. With another framework, call verify(CALLBACK_URL, rawQuery, signatureHeader, keyIdHeader).

Payment data

PropertyValue
valueCoinAmount received. Card payments arrive in USDC (or USDT) on Polygon after the provider's fee. Crypto payments are in the coin paid.
coinpolygon_usdc, btc, erc20_usdt…
txidInTransaction that paid the order's address. Credit each one once.
txidOutTransaction that forwarded the funds to you.
addressInThe order's deposit address.
valueForwardedCoinTotal forwarded to you and to any affiliates.
addressOutShare received by each payout address.
payouts()Exact amount sent to each address, computed without floating point.
pendingTrue for an unconfirmed crypto notice.
get('order')Any of your own parameters.

White label and affiliates

You can earn a commission on the sales of merchants you onboard. The shares must add up to 0.99, because PayGate.to keeps 1%. You can also split each payment across up to 19 wallets. See the white-label program.

// Commission: 2% to you, 97% to the merchant
await paygate.card.createAffiliateWallet(merchantWallet, callback, yourWallet, { affiliateFee: 0.02, merchantFee: 0.97 });
await paygate.crypto.createAffiliateWallet('trc20_usdt', merchantWallet, callback, yourWallet, { affiliateFee: 0.02, merchantFee: 0.97 });

// Split payouts
await paygate.card.createMassPayoutWallet(callback, { '0xPartnerA': 0.5, '0xPartnerB': 0.49 });

// Branding on the hosted pages
paygate.card.paymentUrl(wallet, '25', email, 'USD', {
  branding: { logo: 'https://yourshop.com/logo.png', background: '#ffffff', theme: '#7c3aed', button: '#111827' },
});

Coin data and status

await paygate.crypto.coins();                   // every coin with name, logo and minimum
await paygate.crypto.info('erc20_usdc');        // name, minimum, prices in fiat currencies
await paygate.crypto.price('btc', 'EUR');
await paygate.crypto.fees('btc');               // estimated network fee, in BTC and in fiat
await paygate.crypto.convert('sol_sol', '20', 'GBP');

// Occasional checks only: these endpoints are rate limited. Update orders from the callback.
await paygate.card.paymentStatus(ipnToken);
await paygate.crypto.paymentStatus(ipnToken, 'bep20_usdt');

Amounts are plain decimal strings ('0.00001', never 1e-5), so they keep their full precision.

Configuration and errors

new PayGate({
  apiDomain: 'api.paygate.to',
  checkoutDomain: 'checkout.paygate.to',
  timeoutMs: 30000,
  retries: 1,          // GET only; POST and swaps are never retried
  fetch: myFetch,      // any WHATWG fetch; defaults to the global fetch
});
ErrorWhen
InvalidArgumentErrorInput rejected before any request: a callback without an order parameter, shares that do not add up to 0.99, a bad currency…
ApiErrorPayGate.to answered with an error, a non-2xx status or a body that is not JSON (statusCode, responseBody).
TransportErrorThe request never completed (DNS, TLS, timeout).

All three extend PayGateError. Messages never include query strings, because they contain wallets and callback tokens.

Examples and testing

The repository's examples/express folder is a small working Express shop: a card checkout, crypto on your own page, the hosted page, and a callback handler that checks the signature, the token, repeats, the coin and the amount. examples/http-callback.mjs shows a callback endpoint built with only Node's http module.

npm test                                         # build, then unit tests (no network)
PAYGATE_INTEGRATION=1 npm run test:integration   # read-only calls to the live API

In your own tests, pass a fake fetch. To test a callback handler, use your own key pair: new CallbackVerifier({ keys: { v1: testPublicPem } }), and sign the URL with crypto.sign('sha256', …).

Security checklist

  • Always verify the signature, and answer 403 when it fails. A callback that has not been verified proves nothing; the User-Agent header can be set by anyone.
  • Keep the callback secret on your server only. It stops a valid payment for one order from being replayed against another.
  • Credit each incoming transaction (txid_in) once. Callbacks are retried and can arrive more than once.
  • Compare the coin and the amount received with the order before releasing goods. Customers can see and edit the card payment link.
  • Use a self-custodial wallet you control for payouts.

The signing key is published at /docs/paygate_callback_public.pem and bundled in the SDK as v1. The full scheme is described in the Callback Event reference.