mrkeyoor.com_
Wed 05 Aug 05:05 UTC
npmInfraupdated 05 Aug 2026

stripe

stripe (stripe-node) is Stripe's official server-side SDK for Node.js. It wraps the entire Stripe REST API (payments, customers, subscriptions, invoices, Connect, Checkout) in typed resource methods like stripe.paymentIntents.create(), and adds the operational pieces you would otherwise hand-roll: automatic retries with idempotency keys, webhook signature verification, auto-pagination, and TypeScript types generated from the latest API version. It is server-only; card collection in the browser is Stripe.js, a different package.

Verdict

If you use Stripe on a Node backend there is no real debate: this is the SDK, it is actively maintained (pushed within the last day), and the retry/webhook/pagination helpers are worth it. Budget real time for webhook raw-body handling and for TypeScript churn across API version upgrades.

API stability3/5Majors track Stripe API versions, so breaking releases are frequent by policy (v22 now), and even minor bumps may introduce new type errors the README explicitly says you must resolve with type guards.
Docs5/5Stripe's API reference has per-method Node examples for every endpoint, plus a long README covering retries, webhooks, pagination, proxies, and TypeScript edge cases.
Maintenance5/5Pushed August 4, 2026, releases land near-weekly with the API, and only 49 issues are open on a 17.5M weekly download package.
Ecosystem4/517.5M weekly downloads, official samples repos, Stripe CLI tooling, and framework guides everywhere; the ecosystem is deep but entirely bound to one vendor.

Use it if

  • You are accepting payments with Stripe from a Node, Deno, or serverless backend; this is the official, always-current SDK
  • You want webhook signature verification (webhooks.constructEvent) instead of writing your own HMAC check
  • You need to walk large lists (customers, charges) and want for-await auto-pagination instead of manual cursor handling
  • You want typed request/response params: TypeScript definitions ship with the package and track the latest Stripe API version
Skip it if

Setup reality

npm install stripe is trivial (zero runtime dependencies, Node 18+ required) but the integration around it is where the time goes. You need a secret key from the dashboard, and since v17 instantiating the client at import time without the key present breaks build steps, so you end up writing a lazy getter. Webhooks are the classic trap: constructEvent needs the raw request body, so Express/Next.js body parsing must be disabled for that route or verification fails. Telemetry to Stripe is on by default. Testing means juggling sk_test keys and the Stripe CLI for webhook forwarding.

Patterns

Initialize the client safelyinit-client

import Stripe from 'stripe';

let _stripe: Stripe | null = null;
export const getStripe = (): Stripe => {
  if (!_stripe) {
    _stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string);
  }
  return _stripe;
};

Since v17, constructing Stripe at module top level fails builds where the env var is absent; the README recommends this lazy pattern or a placeholder key.

Create a PaymentIntentcreate-payment-intent

const intent = await stripe.paymentIntents.create({
  amount: 2000, // smallest currency unit: 2000 = $20.00
  currency: 'usd',
  automatic_payment_methods: { enabled: true },
});
// send intent.client_secret to the browser for Stripe.js to confirm

Amounts are integers in the smallest currency unit; passing 20.00 for twenty dollars is the classic first-week bug.

Verify a webhook signature (Express)verify-webhook

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body, // raw Buffer, not parsed JSON
      req.headers['stripe-signature'],
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  if (event.type === 'payment_intent.succeeded') { /* fulfill */ }
  res.json({ received: true });
});

constructEvent needs the raw body exactly as received; any global express.json() middleware on this route breaks verification with a signature error.

Create a hosted Checkout sessioncreate-checkout-session

const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: 'price_123', quantity: 1 }],
  success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}',
  cancel_url: 'https://example.com/cancel',
});
// redirect the user to session.url

The literal {CHECKOUT_SESSION_ID} placeholder is substituted by Stripe; do not template it yourself.

Iterate all results with auto-paginationauto-paginate-list

for await (const customer of stripe.customers.list({ limit: 100 })) {
  await handle(customer);
  if (shouldStop()) break;
}

// or collect a bounded batch:
const charges = await stripe.charges.list().autoPagingToArray({ limit: 1000 });

autoPagingToArray refuses unbounded collection; you must pass a limit, and for-await is the safer default for big lists.

Make a create call idempotentidempotency-key

const charge = await stripe.paymentIntents.create(
  { amount: 2000, currency: 'usd', automatic_payment_methods: { enabled: true } },
  { idempotencyKey: `order-${orderId}` }
);

Reusing the same key within 24 hours returns the original response instead of double-charging; derive it from your own order ID, not a random UUID per attempt.

Configure automatic network retriesconfigure-retries

const stripe = new Stripe(key, {
  maxNetworkRetries: 2, // exponential backoff
  timeout: 20000,       // default is 80000 ms
});

// per-request override:
await stripe.customers.create({ email }, { maxNetworkRetries: 3 });

Since v13 the default is already 1 retry with idempotency keys added automatically; set 0 to disable, and remember the default timeout is a long 80 seconds.

Expand a nested object in one callexpand-nested-objects

const intent = await stripe.paymentIntents.retrieve('pi_123', {
  expand: ['customer', 'latest_charge'],
});
const email = (intent.customer as Stripe.Customer).email;

Expandable fields are typed as string | Object, so TypeScript forces a cast; without expand you only get the id string.

Act on behalf of a Connect accountconnect-account-call

const transactions = await stripe.balanceTransactions.list(
  { limit: 10 },
  { stripeAccount: 'acct_1ABC...' }
);

stripeAccount is a per-request option, not a param; putting it inside the first argument is silently wrong.

Handle declined cards and API errorshandle-card-errors

try {
  await stripe.paymentIntents.confirm('pi_123');
} catch (err) {
  if (err instanceof Stripe.errors.StripeCardError) {
    console.log(err.code, err.decline_code); // e.g. 'card_declined', 'insufficient_funds'
  } else if (err instanceof Stripe.errors.StripeRateLimitError) {
    // back off and retry
  } else {
    throw err;
  }
}

Card declines are thrown as errors, not returned as statuses; log err.requestId when contacting Stripe support.

Mock a signed webhook event in teststest-webhook-locally

const payload = JSON.stringify({ id: 'evt_test', object: 'event' });
const header = stripe.webhooks.generateTestHeaderString({
  payload,
  secret: 'whsec_test_secret',
});
const event = stripe.webhooks.constructEvent(payload, header, 'whsec_test_secret');

This tests your verification path without the Stripe CLI; for live-ish local testing use 'stripe listen --forward-to localhost:3000/webhook'.

Alternatives

PackageRegistryPick it when
braintreenpmYour processor is Braintree/PayPal; its official Node SDK covers similar ground for that platform
squarenpmYou are on Square for payments, especially with in-person/POS needs
@paypal/paypal-server-sdknpmYou only need PayPal buttons and orders rather than a full billing stack