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

@pandait.tech/payment-nuvei

v1.0.1

Published

Nuvei Ecuador payment gateway adapter for Next.js: charge handler, webhook, 3DS flow, refunds, tokenized cards, and payment links. Part of panda-commerce-kit.

Readme

@pandait.tech/payment-nuvei

Nuvei Ecuador payment gateway adapter for Next.js. Ships:

  • SDK wrapper for Nuvei's REST endpoints (debit, verify, refund, card list/delete).
  • Route-handler factories for /api/payment/* and /api/webhooks/nuvei — drop-in for Next.js App Router.
  • Payment-link primitives (token gen, expiry helpers, anonymous-auth helper).
  • React UI components (CardVisual, SavedCards, NuveiPaymentForm) with bank-color branding driven by the package's BIN/bank-colors database.

Extracted from pauhenriques-website (production-validated).

Status

V0 — Phase 1 of panda-commerce-kit. Published to public npm.

Install

pnpm add @pandait.tech/payment-nuvei

Peer deps the consumer must install:

| Peer | When required | |---|---| | next >= 14 | Always (handlers + UI use next/server and next/script) | | react >= 18 | Only if importing /ui | | react-icons >= 5 | Only if importing /ui | | zod >= 3.22 | Always (used by 3ds-complete handler validation) | | firebase-admin >= 12 | Always (handlers need Firestore + Auth admin SDKs) | | firebase >= 10 | Only if importing createAnonymousAuthHelper |

Public API

// Pure SDK functions (server-side)
import {
  debitWithToken,
  verifyThreeDS,
  refundTransaction,
  listCards,
  deleteCard,
  verifyCard,
} from "@pandait.tech/payment-nuvei";

// Route handler factories
import {
  createChargeHandler,
  createWebhookHandler,
  create3dsCallbackHandler,
  create3dsCompleteHandler,
  create3dsTimeoutHandler,
  createRefundHandler,
  createInitCheckoutHandler,
  createCardsHandler,
  createVerifyHandler,
  createTestChargeHandler,
} from "@pandait.tech/payment-nuvei/handlers";

// Payment-link primitives
import {
  generatePaymentLinkToken,
  defaultExpiry,
  isExpired,
  createAnonymousAuthHelper,
  PAYMENT_LINK_PRICE_MIN,
  PAYMENT_LINK_PRICE_MAX,
} from "@pandait.tech/payment-nuvei/payment-links";

// React UI components
import {
  CardVisual,
  SavedCards,
  NuveiPaymentForm,
  // utilities
  getBinDatabase,
  lookupBin,
  lookupBankPalette,
} from "@pandait.tech/payment-nuvei/ui";

Handler factories — usage

// app/api/payment/charge/route.ts
import { createChargeHandler } from "@pandait.tech/payment-nuvei/handlers";
import { dbAdmin, authAdmin } from "@/lib/firebase";
import { sendPaymentConfirmation, sendPaymentFailed } from "@/lib/email";
import { getPriceDisplay } from "@/lib/pricing";

export const POST = createChargeHandler({
  firebase: { db: dbAdmin, auth: authAdmin },
  email: { sendPaymentConfirmation, sendPaymentFailed },
  getPriceDisplay,
  merchantName: "Acme Shop",
  // Required for 3DS to work. Without it, browser-info is sent to Nuvei but
  // 3DS challenges won't initiate — the handler logs a loud error if browserInfo
  // is provided but cloudFunctionsBaseUrl is missing.
  cloudFunctionsBaseUrl: process.env.CLOUD_FUNCTIONS_BASE_URL,
  rateLimit: checkRateLimit,         // optional
  turnstile: verifyTurnstile,        // optional
  // Optional consumer-specific side effects after order transitions to paid
  onPaymentSucceeded: async (order) => {
    await ensureFulfillment(order.id);
  },
  // Optional retry URL for payment-failed emails
  getRetryUrl: (order) => `https://shop.example.com/checkout?orderId=${order.id}`,
});

Apply the same pattern to the remaining handlers — each one is documented inline with its own XHandlerDeps interface.

The webhook is OPTIONAL — and if you expose it, secure it

First, set expectations correctly:

  • Payments are confirmed by the synchronous /debit response (createChargeHandler reads transaction.status === "success" && status_detail === 3), plus the 3DS term_url callback + verify polling for challenged transactions. That path fully confirms a payment without any webhook.
  • Paymentez's transaction-notification callback (developers.paymentez.com/api/#webhook) is an optional, redundant async channel — Paymentez POSTs transaction status to a URL you register with Paymentez during integration (there is no documented self-serve dashboard; for many accounts it is simply never wired up). In a real production consumer we observed Paymentez never calling it at all.
  • So createWebhookHandler is a reconciliation backstop you can mount if your Paymentez account is configured to notify it. It is not required to take payments.

Because the endpoint is nonetheless a public POST route, if you mount it you must authenticate it — otherwise anyone who knows an orderId could POST a fake "paid" notification (status_detail: 3) and mark an order paid. Paymentez callbacks are not signed, so use a shared secret:

  1. Pass a secret to the handler (or via NUVEI_WEBHOOK_SECRET):

    export const POST = createWebhookHandler({
      firebase: { db: dbAdmin, auth: authAdmin },
      email: { sendPaymentConfirmation, sendPaymentPending },
      webhookSecret: process.env.NUVEI_WEBHOOK_SECRET,
      onPaymentSucceeded: async (order) => { /* enrollment, fulfillment, ... */ },
    });
  2. If/when you register the notification URL with Paymentez, append the secret as a query param:

    https://your-shop.com/api/webhooks/nuvei?key=YOUR_SECRET

    (The secret is also accepted as the x-webhook-key header.)

Requests without a matching key get 401 and are not processed (constant-time compared). If webhookSecret is unset, the handler still processes but logs a loud warning. If you never register the URL with Paymentez, setting the secret simply seals the endpoint (everything → 401), which is a safe default.

onPaymentSucceeded fires once, on the transition into paid (idempotent across retries).

Hosting & deployment (3DS callback + Nuvei egress)

This package is hosting-agnostic by design — the payment logic doesn't care where it runs. But two parts of the Nuvei flow may need to run outside your app's request edge depending on where you host, because of how the host's infrastructure (not this package) handles certain traffic:

1. The 3DS callback (termUrl)

After a 3DS challenge, the bank's ACS makes an external, cross-origin POST to the termUrl. The termUrl is built by createChargeHandler as:

${cloudFunctionsBaseUrl}/threeDSCallback?orderId=<id>

The callback must: read the cres from the POST (form-urlencoded or JSON, casing varies by issuer), decode transStatus from the base64url CRES payload, persist threeDSCres + threeDSTransStatus on the order (only while status === "3ds-pending"), and return HTML that postMessages { type: "3DS_COMPLETE", orderId, transStatus } to the parent checkout window (no redirect).

| Your host | How to expose the callback | |---|---| | Firebase App Hosting / Cloud Run | App Hosting returns 403 to the bank's external POST before your code runs (infra-level block). You must deploy the callback as a standalone Cloud Function with permissive ingress (cors: true) and point cloudFunctionsBaseUrl at https://<region>-<project>.cloudfunctions.net. The package's create3dsCallbackHandler (a Next.js route) is not usable as termUrl here. | | Vercel / Netlify / plain Node / properly-configured Cloud Run | The Next.js route from create3dsCallbackHandler accepts the external POST fine. Mount it at /api/payment/3ds-callback and set cloudFunctionsBaseUrl to your own origin + that path. |

2. Nuvei API egress

Some hosts can't reach Nuvei's API directly. Cloud Run (App Hosting) returns 500 calling ccapi.paymentez.com (egress/TLS incompatibility). Work around it with a tiny proxy (a Cloud Function works) and set the NUVEI_PROXY_URL env — the SDK routes through it automatically when present. On hosts without this limitation, leave NUVEI_PROXY_URL unset and the SDK calls Nuvei directly.

Reference implementation

A production-validated implementation of both (threeDSCallback + nuveiProxy, Firebase Functions v2) lives in pauhenriques-website/functions/index.js. The logic is business-agnostic — copy it and point your env vars at the deployed functions.

Roadmap (v0.next): these will ship from the package as framework-neutral handlers (Web-standard Request → Response) plus thin runtime adapters (toCloudFunction, toExpress), so a single source of truth can deploy to a Next.js route, a Cloud Function, or any runtime — eliminating the hand-maintained functions/index.js duplication. The package will stay agnostic to hosting and framework; it remains coupled to Firebase (Firestore/Auth) as the data layer by design.

UI components — usage

All /ui exports are client components ("use client"). They consume CSS variables for theming so the same components work for every white-label client with their own branding.

1. Load the styles

You have two options. Option A is recommended — it works without Tailwind, on any Tailwind version, and doesn't require configuring your build to scan this package.

Option A — import the prebuilt stylesheet (recommended, standalone):

// once, e.g. in app/layout.tsx or your global entry
import "@pandait.tech/payment-nuvei/ui/styles.css";

This ships the exact utility classes the components use (no Tailwind Preflight/reset — it won't touch your page's base styles). You still define the brand tokens (step 2) for theming.

Option B — let your own Tailwind build the classes (Tailwind v4 users):

Skip the CSS import and instead point Tailwind at the package so it generates the classes itself. Tailwind v4 (@source in your CSS):

@import "tailwindcss";
@source "../node_modules/@pandait.tech/payment-nuvei/dist/**/*.{js,cjs}";

Tailwind v3 (content in tailwind.config):

export default {
  content: [
    "./app/**/*.{ts,tsx}",
    "./node_modules/@pandait.tech/payment-nuvei/dist/**/*.{js,cjs}",
  ],
};

If components render unstyled, you forgot Option A's import or Option B's @source/content entry. Option A removes that failure mode entirely.

2. Define CSS variables in your global stylesheet

Add this to your app/globals.css (Next.js) or equivalent, in :root:

:root {
  /* Brand */
  --color-primary: #a68a63;
  --color-primary-hover: #b89a73;

  /* Text */
  --color-text-main: #c1c4a7;

  /* Surfaces */
  --color-background: #343d2a;
  --color-surface-elevated: #475536;

  /* Borders */
  --color-border-default: rgba(193, 196, 167, 0.20);
  --color-border-strong: rgba(193, 196, 167, 0.35);
  --color-border-subtle: rgba(193, 196, 167, 0.12);

  /* Status */
  --color-error: #c75c4a;
  --color-warning: #c9a84c;
}

/* Optional: dark mode palette */
.dark {
  --color-primary: #b89a73;
  /* ...override as needed */
}

3. Use the components

"use client";
import { SavedCards, NuveiPaymentForm } from "@pandait.tech/payment-nuvei/ui";

export function PaymentMethodPicker() {
  const [token, setToken] = useState<string | null>(null);
  return (
    <div>
      <SavedCards
        selectedToken={token}
        onSelectCard={(card, cvc) => { /* ... */ }}
        onAddNewCard={() => { /* show NuveiPaymentForm */ }}
      />
      <NuveiPaymentForm
        uid={user.uid}
        email={user.email}
        onTokenSuccess={(token, cardInfo, saveCard) => { /* call /api/payment/charge */ }}
        onTokenError={(err) => { /* ... */ }}
      />
    </div>
  );
}

The NuveiPaymentForm reads NEXT_PUBLIC_NUVEI_CLIENT_APP_CODE, NEXT_PUBLIC_NUVEI_CLIENT_APP_KEY, and NEXT_PUBLIC_NUVEI_ENV from your env by default. You can override per-instance with the nuveiAppCode / nuveiAppKey / nuveiEnv props.

Endpoints the UI components expect

The components POST/GET to fixed paths because the package's handler factories are designed to mount there:

| Component / call | Endpoint | Handler factory | |---|---|---| | SavedCards GET on mount | /api/nuvei/cards | createCardsHandler GET | | SavedCards DELETE on remove | /api/nuvei/cards | createCardsHandler DELETE | | SavedCards POST on verify | /api/nuvei/verify | createVerifyHandler POST | | NuveiPaymentForm DELETE on duplicate | /api/nuvei/cards | createCardsHandler DELETE | | CardVisual BIN lookup | /api/bins (overridable) | Consumer-provided JSON of BinDatabase |

The consumer must wire those routes — each route.ts is one or two lines re-exporting the factory result.

CardVisual degrades gracefully if no BIN endpoint is served: it just renders without bank-specific colors. The endpoint is optional and overridable via the binDatabaseEndpoint prop (default /api/bins).

Firestore data contract

The handlers read/write a single orders collection (plus promotions for coupon usage). Your Firestore schema must match these names, statuses, and fields:

orders/{orderId} — the document the charge/webhook/3DS/refund handlers operate on. dev_reference sent to Nuvei is the orderId.

| Field | Written by | Notes | |---|---|---| | status | all | lifecycle: pending3ds-pendingpaid | cancelled | refunded. Final statuses (paid/delivered/shipped) are never downgraded by the webhook. | | userId | consumer (at create) | must match the __session cookie uid; handlers authorize against it. | | userEmail | consumer | used for confirmation emails. | | items, subtotal, vat, total, discount | consumer | echoed into emails. | | promotionId, couponCode | consumer | if set, webhook/charge increment promotions/{id}.currentUses and write a promotions/{id}/usages/{auto} doc atomically. | | paymentTransactionId, authorizationCode, nuveiTransactionId | charge/webhook | Nuvei refs; paymentTransactionId is required for refunds. | | threeDSCres, threeDSTransStatus | 3DS callback / webhook | set during the challenge; cleared on completion. | | verifyCalledAt | webhook | guards the BY_CRES verify path (single-fire). | | emailPending, missingAuthCodeFlagged | charge/webhook | pending-email reconciliation when auth_code arrives late. | | deleteCardAfterPayment, paymentToken | charge | "don't save card" opt-out, honored on 3DS/webhook completion. | | shippingAddress.fullName | consumer | used as the email customer name. | | refundedAt | refund | set on successful refund. |

promotions/{promotionId}: currentUses (incremented). promotions/{promotionId}/usages/{auto}: { userId, orderId, discountApplied, usedAt }.

Business-specific side effects (course enrollment, fulfillment, paymentLink usage) live in your onPaymentSucceeded callback — the package never assumes those collections. onPaymentSucceeded fires once, on the transition into paid (idempotent across Nuvei webhook retries), but you should still make it internally idempotent as defense-in-depth.

API surface & stability

Public, supported entry points: the package root (SDK), ./handlers, ./adapters, ./payment-links, ./ui, and ./ui/styles.css. Everything else (e.g. internal http.ts) is private and may change without notice.

As of 1.0.0 the public API is frozen under semver — breaking changes to the exported factories, their *HandlerDeps interfaces, the SDK functions, or the UI component props require a major version bump. See CHANGELOG.md.

Design notes consumers should know:

  • Order shape is generic. MinimalOrder extends Record<string, unknown> and declares only fields the package itself reads/writes. Your business-specific order fields are yours to read off the order inside your callbacks (onPaymentSucceeded, validateCustomOrder, getRetryUrl) — narrow them there.
  • Payment links are primitives only. The package ships token/expiry/price-bound helpers; the payment-link document shape (what it points to) is yours to define.
  • The webhook secret is optional but strongly recommended. It's left optional so the package never forces a specific webhook-URL scheme, but an unconfigured webhook is forgeable — always set webhookSecret in production (see "Securing the webhook" above).

Migration from pauhenriques-website

See per-file Source: pauhenriques-website/... headers throughout the package. The refactor preserves behavior with three intentional fixes vs. the original (documented in commit 4b0cb06 — coupon-reuse in webhook BY_CRES + card-delete on 3DS flows).