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

feathers-stripe

v4.1.0

Published

A Feathers service for Stripe

Readme

feathers-stripe

CI npm Dependency Status Download Status Discord

Feathers services for Stripe

Installation

npm install feathers-stripe stripe

feathers-stripe is ESM only and requires Node.js 24 or later, stripe@^23 and @feathersjs/feathers@^5.

Usage

Every service takes a stripe instance and is registered like any other Feathers service:

import { Stripe } from 'stripe'
import { CustomerService } from 'feathers-stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

app.use('stripe/customers', new CustomerService({ stripe }))

const customer = await app
  .service('stripe/customers')
  .create({ email: '[email protected]' })

Protect these services! They act on your Stripe account with your secret key. Unless a user is explicitly allowed to, block all external access (REST, websockets) and only call them from your own code. See Security.

Method mapping

| Feathers | Stripe | | -------- | --------------------------------------------------------------------------------- | | find | list | | get | retrieve | | create | create | | update | update | | patch | update, with some special cases (see Available services) | | remove | del, or the closest equivalent like cancel, void or expire | | search | search (only Customer and Invoice) |

Methods that Stripe doesn't support for a resource reject with a NotImplemented error.

Request options

Use params.stripe to pass request options like stripeAccount, idempotencyKey or apiVersion:

await app
  .service('stripe/customers')
  .create(
    { email: '[email protected]' },
    { stripe: { stripeAccount: 'acct_123', idempotencyKey: 'signup-42' } },
  )

Querying

params.query is passed to Stripe's list method. Feathers style $ prefixes are removed, so $limit becomes limit and { created: { $gt: 1700000000 } } becomes { created: { gt: 1700000000 } }.

const invoices = await app.service('stripe/invoices').find({
  query: { customer: 'cus_123', status: 'open', $limit: 50 },
})

Pagination

find returns Stripe's list object, not the Feathers pagination format:

{ object: 'list', data: [/* ... */], has_more: true, url: '/v1/invoices' }

To get the next page, pass the id of the last item as starting_after. $limit is capped at 100, which is Stripe's maximum. To change that, pass your own paginate option to the service.

With paginate: false, find fetches all pages and returns a plain array:

const allInvoices = await app.service('stripe/invoices').find({
  query: { customer: 'cus_123' },
  paginate: false,
})

Expanding responses

Use params.query.expand to expand related objects. For get only expand is passed on. For find the whole query is passed on, so the paths need the data. prefix:

await app.service('stripe/customers').get('cus_123', {
  query: { expand: ['default_source'] },
})

await app.service('stripe/invoices').find({
  query: { expand: ['data.subscription'] },
})

Search

CustomerService and InvoiceService have a search method for Stripe's search query language:

const result = await app.service('stripe/customers').search({
  query: { query: "email:'[email protected]'" },
})

search is a custom method. To call it from a client, add it to the service's methods when registering it.

Errors

Stripe errors are converted to Feathers errors. The original Stripe error is available as error.data.

| Stripe | Feathers | | --------------------------- | ------------------ | | StripeCardError | PaymentError | | StripeInvalidRequestError | BadRequest | | StripeAPIError | Unavailable | | StripeConnectionError | Unavailable | | StripeAuthenticationError | NotAuthenticated | | StripePermissionError | Forbidden | | StripeRateLimitError | TooManyRequests | | anything else | GeneralError |

If a required parent id like customer is missing, the service rejects with a BadRequest before calling Stripe.

Available services

Services for nested Stripe resources need the id of their parent object. It is passed in params.query or data as noted below.

| Service | Stripe resource | Notes | | ------------------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | AccountService | accounts | | | AccountLinkService | accountLinks | create only | | ApplicationFeeRefundService | applicationFees.*Refund* | Requires query.fee | | BalanceService | balance | get only. The id is used as stripeAccount for connected accounts | | BalanceTransactionService | balanceTransactions | find and get only | | BankAccountService | customers.*Source* | Deprecated, use PaymentMethodService. Requires query.customer | | BillingMeterService | billing.meters | remove deactivates the meter | | BillingMeterEventService | billing.meterEvents | create only | | ChargeService | charges | patch with { capture: true } captures. remove refunds the charge | | CheckoutSessionService | checkout.sessions | remove expires the session | | CouponService | coupons | | | CreditNoteService | creditNotes | remove voids the credit note | | CustomerService | customers | Supports search | | CustomerPortalConfigurationService | billingPortal.configurations | | | CustomerPortalSessionService | billingPortal.sessions | create only | | CustomerSessionService | customerSessions | create only | | DisputeService | disputes | | | EventService | events | find and get only | | ExternalAccountService | accounts.*ExternalAccount* | Requires query.account, for create data.account | | InvoiceService | invoices | patch with { pay: true } pays the invoice. Supports search | | InvoiceItemService | invoiceItems | | | PaymentIntentService | paymentIntents | patch with { capture: true }, { confirm: true } or { cancel: true } captures, confirms or cancels. Other fields are passed to Stripe | | PaymentLinkService | paymentLinks | | | PaymentMethodService | paymentMethods | patch with { attach: true, customer } attaches. remove detaches | | PayoutService | payouts | remove cancels the payout | | PlanService | plans | Deprecated, use PriceService | | PriceService | prices | | | ProductService | products | | | PromotionCodeService | promotionCodes | | | RefundService | refunds | | | SetupIntentService | setupIntents | | | SourceService | sources | Deprecated, use PaymentMethodService. create with data.customer attaches to a customer | | SubscriptionService | subscriptions | remove cancels the subscription | | SubscriptionItemService | subscriptionItems | | | SubscriptionScheduleService | subscriptionSchedules | remove cancels the schedule | | TaxRateService | taxRates | | | TokenService | tokens | get and create only | | TransferService | transfers | remove reverses the transfer | | TransferReversalService | transfers.*Reversal* | Requires query.transfer, for create data.transfer | | WebhookService | - | See Webhooks |

Example for a nested resource:

app.use('stripe/external-accounts', new ExternalAccountService({ stripe }))

const bankAccount = await app
  .service('stripe/external-accounts')
  .create({ account: 'acct_123', external_account: 'btok_123' })
// stripe.accounts.createExternalAccount('acct_123', { external_account: 'btok_123' })

await app.service('stripe/external-accounts').get('ba_123', {
  query: { account: 'acct_123' },
})
// stripe.accounts.retrieveExternalAccount('acct_123', 'ba_123')

Webhooks

setupWebhook registers a webhook service at the given route. It only allows create and verifies the stripe-signature header of every request against the raw request body before any handler runs. The verified event is passed to the service as params.stripeEvent. External calls without a verified event (e.g. via websockets) are rejected with NotAuthenticated.

Stripe signs the raw request body, so keep it around when parsing JSON:

import { json } from '@feathersjs/express'

app.use(
  json({
    verify: (req, res, buf) => {
      req.rawBody = buf
    },
  }),
)

Then register the webhook with handlers for the event types you need. A handler for customer.subscription.created is looked up at handlers.customer.subscription.created. The return value of the handler is sent back to Stripe. Events without a handler resolve with {}.

import { Stripe } from 'stripe'
import { setupWebhook } from 'feathers-stripe'

const service = setupWebhook(app, 'stripe/webhook', {
  endpointSecret: process.env.STRIPE_WEBHOOK_SECRET,
  stripe: new Stripe(process.env.STRIPE_SECRET_KEY),
  handlers: {
    customer: {
      subscription: {
        async created({ object, event, params, app }) {
          // object is the subscription
          return {}
        },
      },
    },
  },
})

// `service` is the registered service, so hooks can be added
service.hooks({ before: { create: [logEvent] } })

The webhook service must stay reachable from outside. Don't add the internal-only hook from Security to it.

Security

Block external access to all Stripe services that users shouldn't call directly:

import { Forbidden } from '@feathersjs/errors'

const internalOnly = async (context) => {
  if (context.params.provider) {
    throw new Forbidden('You are not allowed to access this')
  }
}

app.service('stripe/customers').hooks({
  before: { all: [internalOnly] },
})

Rate limiting

Stripe limits the number of requests per second. An around hook with a rate limiter like Bottleneck keeps you below that:

import Bottleneck from 'bottleneck'

// about 100 requests per second in live mode. Use { minTime: 40 } for test mode
const readLimiter = new Bottleneck({ minTime: 10 })
const writeLimiter = new Bottleneck({ minTime: 10 })

const rateLimit = async (context, next) => {
  const limiter =
    context.method === 'find' || context.method === 'get'
      ? readLimiter
      : writeLimiter

  await limiter.schedule(() => next())
}

app.service('stripe/customers').hooks({
  around: { all: [rateLimit] },
})

Complete example

A Feathers app that creates payment intents from its own orders service and handles the result in a webhook. The Stripe services themselves are not reachable from outside.

import { feathers } from '@feathersjs/feathers'
import express, {
  errorHandler,
  json,
  notFound,
  rest,
  urlencoded,
} from '@feathersjs/express'
import { Forbidden } from '@feathersjs/errors'
import { Stripe } from 'stripe'
import { PaymentIntentService, setupWebhook } from 'feathers-stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

const app = express(feathers())

app.use(
  json({
    verify: (req, res, buf) => {
      req.rawBody = buf
    },
  }),
)
app.use(urlencoded({ extended: true }))
app.configure(rest())

app.use('stripe/payment-intents', new PaymentIntentService({ stripe }))

app.service('stripe/payment-intents').hooks({
  before: {
    all: [
      async (context) => {
        if (context.params.provider) {
          throw new Forbidden('You are not allowed to access this')
        }
      },
    ],
  },
})

// Your own service decides what a user may pay for
app.use('orders', {
  async create(data) {
    const paymentIntent = await app.service('stripe/payment-intents').create({
      amount: 2000,
      currency: 'eur',
      automatic_payment_methods: { enabled: true },
      metadata: { product: data.product },
    })

    // The client confirms the payment with Stripe.js
    return { clientSecret: paymentIntent.client_secret }
  },
})

setupWebhook(app, 'stripe/webhook', {
  endpointSecret: process.env.STRIPE_WEBHOOK_SECRET,
  stripe,
  handlers: {
    payment_intent: {
      async succeeded({ object }) {
        console.log('Payment succeeded', object.id)
        return {}
      },
    },
  },
})

app.use(notFound())
app.use(errorHandler())

await app.listen(3030)

Upgrading from v3

  • feathers-stripe is ESM only and requires Node.js 24 or later.
  • stripe@^23 is required. Stripe no longer accepts params and request options in the same argument, see the migration guide. feathers-stripe passes them correctly now.
  • expand is no longer a request option. Use params.query.expand instead of params.stripe.expand. The old way still works but is deprecated.
  • setupWebhook registers the webhook service itself and needs req.rawBody. Remove your own app.use(route, ...) for it. The signature is now checked for every external request.
  • CardService and TransactionService are removed. Use PaymentMethodService and BalanceTransactionService.
  • Service methods always return a promise. NotImplemented and BadRequest reject instead of being thrown synchronously.
  • A missing parent id (customer, account, transfer, fee) rejects with a BadRequest instead of calling Stripe.
  • ApplicationFeeRefundService expects the fee id in params.query.fee for all methods, no longer in params.stripe.fee. The ParamsWithStripeFee type is removed.
  • StripePermissionError is converted to Forbidden instead of GeneralError.

License

Licensed under the MIT license.