Skip to content

Bachs ​

Bachs is a billing and payments platform for hosted checkout, product-based payments, webhook-confirmed collections, refunds, multi-currency payment flows, and marketplace payouts through Bachs Connect.

The @sails-pay/bachs adapter lets Sails applications create Bachs hosted checkouts through the same sails.pay.checkout() API used by other Sails Pay providers.

Provider capabilities ​

Relevant Bachs capabilities include:

  • Checkout Sessions: Create hosted product checkouts from Bachs products or product collections.
  • Pure Checkout: Create hosted checkouts from runtime amount and currency data.
  • Charge verification: Retrieve charge status with sails.pay.verify({ chargeId }).
  • Checkout lookup: Fetch checkout or checkout-session details after redirect or webhook delivery.
  • Customer portal sessions: Create fresh, short-lived billing management URLs with sails.pay.customer.portal({ customerId }).
  • Webhook signatures: Verify X-Bachs-Timestamp and X-Bachs-Signature using HMAC SHA-256.
  • Refunds: Create full or partial refunds for completed charges.
  • Connect: Give recipients balances of their own, fund them, and let them withdraw with sails.pay.connect.

Connect ​

Bachs supports sails.pay.connect (@sails-pay/bachs 0.0.4 and later) for platforms that pay creators, maintainers, contractors, or sellers: connected accounts, hosted onboarding, transfers, balances, and withdrawals.

Getting started with Bachs ​

Before integrating Bachs with Sails Pay:

  1. Create a Bachs account at bachs.io.
  2. Generate a sandbox or live secret API key.
  3. Grant the API key customers:write if your application will create customer portal sessions.
  4. Create products in Bachs if you want product-based checkout sessions.
  5. Configure a webhook destination and copy its signing secret.
  6. Add return and cancel URLs for your application.

TIP

Use sandbox keys starting with sk_sandbox_ during development. The adapter automatically uses https://sandbox-api.bachs.io for sandbox keys unless you provide baseUrl.

Installation ​

Install Sails Pay and the Bachs adapter:

sh
npm i sails-pay @sails-pay/bachs

INFO

Customer portal sessions require @sails-pay/bachs 0.0.2 or later.

Specifying the adapter ​

In config/pay.js, create a payment provider and set the adapter property to @sails-pay/bachs.

js
module.exports.pay = {
  provider: 'default',
  providers: {
    default: {
      adapter: '@sails-pay/bachs'
    }
  }
}

Configuration ​

You can configure the Bachs adapter for both production and local development.

Local development ​

In config/local.js, specify your sandbox values:

js
module.exports = {
  pay: {
    providers: {
      default: {
        adapter: '@sails-pay/bachs',
        apiKey: 'sk_sandbox_xxxxxxxxxxxxxxxxxxxxxx',
        returnUrl: 'http://localhost:1337/payment/return',
        cancelUrl: 'http://localhost:1337/payment/cancel',
        webhookSecret: 'whsec_xxxxxxxxxxxxxxxxxxxxxx'
      }
    }
  }
}

Production ​

For production, use environment variables in config/pay.js:

js
module.exports.pay = {
  provider: 'default',
  providers: {
    default: {
      adapter: '@sails-pay/bachs',
      apiKey: process.env.BACHS_API_KEY,
      baseUrl: process.env.BACHS_BASE_URL,
      returnUrl: process.env.BACHS_RETURN_URL,
      cancelUrl: process.env.BACHS_CANCEL_URL,
      webhookSecret: process.env.BACHS_WEBHOOK_SECRET
    }
  }
}

TIP

Do not commit live API keys or webhook secrets. Keep them in environment variables or your deployment platform's secret store.

Configuring values ​

apiKey (required) ​

Your Bachs secret API key. Sandbox keys start with sk_sandbox_; live keys start with sk_live_.

baseUrl (optional) ​

Override the Bachs API base URL.

When omitted, the adapter chooses the base URL from the API key:

API key prefixBase URL
sk_sandbox_https://sandbox-api.bachs.io
anything elsehttps://api.bachs.io

returnUrl (optional) ​

The URL Bachs redirects to after a Checkout Session payment. Bachs appends ?checkout_id=<id> to the return URL.

successUrl (optional) ​

The URL Bachs redirects to after a Pure Checkout payment. You usually only need returnUrl when using product Checkout Sessions.

cancelUrl (optional) ​

The URL Bachs redirects to when the customer cancels checkout.

webhookSecret (required for webhook verification) ​

The signing secret for your Bachs webhook destination.

Default environment variables ​

If you don't provide configuration values, the adapter will automatically look for these environment variables as fallbacks:

Config ValueEnvironment Variable
apiKeyBACHS_API_KEY
baseUrlBACHS_BASE_URL
returnUrlBACHS_RETURN_URL
successUrlBACHS_SUCCESS_URL
cancelUrlBACHS_CANCEL_URL
webhookSecretBACHS_WEBHOOK_SECRET

Product checkout sessions ​

Use product checkout sessions when your pricing lives in Bachs products.

js
const checkoutUrl = await sails.pay.checkout({
  items: [{ product: 'prod_abc123', quantity: 1 }],
  customer: {
    email: loggedInUser.email,
    name: loggedInUser.fullName
  },
  reference: purchase.reference,
  metadata: {
    purchase: purchase.id.toString(),
    user: loggedInUser.id.toString()
  },
  returnUrl: `${sails.config.custom.baseUrl}/payment/return`,
  cancelUrl: `${sails.config.custom.baseUrl}/payment/cancel`,
  idempotencyKey: purchase.reference
})

The adapter keeps your Sails code camelCase and maps to Bachs snake case at the HTTP boundary:

Sails Pay inputBachs request field
itemsproduct_cart
items[].productproduct_cart[].product_id
productCollectionIdproduct_collection_id
billingCurrencybilling_currency
allowedPaymentMethodTypesallowed_payment_method_types
returnUrlreturn_url
cancelUrlcancel_url
customer.customerIdcustomer.customer_id
customer.phoneNumbercustomer.phone_number
idempotencyKeyIdempotency-Key header

TIP

For product checkout sessions, Bachs requires exactly one of items or productCollectionId.

Guest checkout ​

Starting with @sails-pay/bachs 0.0.8, omit customer, customerEmail, and other customer details on a one-time hosted checkout. Bachs asks the buyer for their name and email on its checkout page. You no longer need a shared placeholder address or an account in your app before someone can pay.

js
const checkoutUrl = await sails.pay.checkout({
  items: [{ product: 'prod_abc123', quantity: 1 }],
  reference: purchase.reference,
  metadata: { purchase: String(purchase.id) },
  returnUrl: `${sails.config.custom.baseUrl}/payment/return`,
  cancelUrl: `${sails.config.custom.baseUrl}/payment/cancel`,
  idempotencyKey: purchase.reference
})

The 0.0.8 adapter leaves customer out of the Bachs request when you supply no identity. Passing a real customer still prefills checkout for a signed-in buyer. Guest checkout is for one-time payments; a checkout containing a recurring product still requires a durable customer identity.

By default, Bachs does not create a customer record for a guest. After they enter their details, customer_details contains the email and name, while customer remains null. Before that point, both can be null. Read the payer's email from a verified collection.succeeded event to send a receipt, and use your own reference or metadata to locate the payment:

js
const data = event.data
const reference = data.reference || data.metadata?.purchase
const guestEmail = data.customer_details?.email

// After verifying the webhook signature, settle the matching payment once.
// A guest's email is for payment and receipts; it does not prove account
// ownership or decide whether their name appears publicly.

If you need the guest in your Bachs customer directory, Bachs also offers customer_creation: "always" on its API. The Sails Pay adapter does not currently expose that option; its default guest behavior keeps customer null. See Bachs' guest checkout guide.

Selection-mode checkout ​

Use productCollectionId when you want customers to choose from a Bachs product collection.

js
const checkoutUrl = await sails.pay.checkout({
  productCollectionId: 'pgrp_1a2b3c4d5e',
  customer: {
    email: loggedInUser.email,
    name: loggedInUser.fullName
  },
  returnUrl: `${sails.config.custom.baseUrl}/payment/return`,
  cancelUrl: `${sails.config.custom.baseUrl}/payment/cancel`
})

Pure checkout ​

Use Pure Checkout when you do not want to pre-create Bachs products.

js
const checkoutUrl = await sails.pay.checkout({
  amount: '50.00',
  currency: 'USD',
  customerEmail: '[email protected]',
  customerName: 'Jane Doe',
  successUrl: `${sails.config.custom.baseUrl}/payment/success`,
  cancelUrl: `${sails.config.custom.baseUrl}/payment/cancel`,
  reference: 'order_9876',
  metadata: {
    order: '9876'
  }
})

For Pure Checkout, the adapter maps successUrl to Bachs success_url.

Checkout lookup ​

After a Checkout Session payment, Bachs redirects the customer to returnUrl with checkout_id in the query string.

js
const checkout = await sails.pay.checkout.get({
  checkoutId: this.req.query.checkout_id
})

The checkout includes charge once the customer submits payment.

Customer portal ​

Create a customer portal session when a customer wants to manage their Bachs billing.

When Bachs is your default provider:

js
const portalUrl = await sails.pay.customer.portal({
  customerId: 'cust_1a2b3c4d5e6f'
})

When Bachs is configured as a named provider:

js
const portalUrl = await sails.pay.provider('bachs').customer.portal({
  customerId: 'cust_1a2b3c4d5e6f'
})

The method calls POST /v1/customers/{customer_id}/portal-sessions without a request body and returns the hosted portal URL as a string.

Required permission ​

The API key must include the customers:write permission. A key without that permission returns a normalized 403 error.

Configuration ​

customer.portal() uses the same API key and base URL resolution as the other Bachs adapter methods:

  • An apiKey or baseUrl passed to the method takes precedence.
  • Otherwise, the method uses the configured Bachs provider values.
  • An sk_sandbox_ key automatically selects https://sandbox-api.bachs.io.
  • An explicit baseUrl is always respected.
js
const portalUrl = await sails.pay.customer.portal({
  customerId: 'cust_1a2b3c4d5e6f',
  apiKey: sails.config.pay.providers.default.apiKey,
  baseUrl: sails.config.pay.providers.default.baseUrl
})

In most applications, configure apiKey and baseUrl once in config/pay.js rather than passing them on every call.

Create sessions on demand ​

Bachs customer portal sessions are short-lived. Create a new session whenever the customer asks to manage billing.

WARNING

Do not cache, persist, or reuse the returned portal URL. The adapter creates a fresh session for every call.

A typical Sails action can redirect the customer immediately:

js
module.exports = {
  friendlyName: 'Open billing portal',

  exits: {
    success: {
      responseType: 'redirect'
    }
  },

  fn: async function () {
    return sails.pay.customer.portal({
      customerId: this.req.me.bachsCustomerId
    })
  }
}

Portal session failures use the Bachs adapter's normalized error path, including 401, 403, 404, 429, 500, and 503 responses. A successful response without a valid url also exits through couldNotCreatePortalUrl.

Charge verification ​

Bachs verifies payments by chargeId, not by checkout reference.

js
const charge = await sails.pay.verify({
  chargeId: 'chr_1a2b3c4d5e6f'
})

If your app only has checkoutId, fetch the checkout first with sails.pay.checkout.get({ checkoutId }) and read checkout.charge.charge_id.

Webhook signature verification ​

Bachs signs webhook deliveries with X-Bachs-Timestamp and X-Bachs-Signature.

js
const isValid = await sails.pay.webhooks.verify({
  rawBody: this.req.rawBody,
  timestamp: this.req.get('X-Bachs-Timestamp'),
  signature: this.req.get('X-Bachs-Signature')
})

WARNING

Webhook verification requires the exact raw request body. If Sails or another body parser has already parsed and re-serialized the JSON body, the signature can fail even when the event is legitimate.

Important payment events include:

  • collection.succeeded
  • collection.failed
  • collection.abandoned
  • collection.underpaid

Always grant access from webhook-confirmed payment state, not from the redirect URL alone.

For route handling, invalid signature responses, and idempotency guidance, see Verifying webhooks.

Refunds ​

Create a refund for a completed charge:

js
const refund = await sails.pay.refund.create({
  chargeId: 'chr_1a2b3c4d5e6f',
  reference: 'refund_9876',
  amount: '25.00',
  reason: 'Customer request',
  idempotencyKey: 'refund_9876'
})

Next steps ​

Additional resources ​

All open source projects are released under the MIT License.