The Merchant Engine Hire Badii

v1.0.1: paying by card at checkout

I’m Badii Boutar, and I build The Merchant Engine alone. v1.0.1 is out, and a shopper can now pay by card at checkout. Stripe is the first card provider. Until this release, the only ways to pay were cash on delivery, manual bank transfer and gift cards, and I said so in every comparison I wrote. That line is now out of date in my own posts, so I fixed them.

What changed for a shopper

Stripe shows up at checkout as a payment method, and the shop owner picks how it looks, per payment method:

  • Embedded. The card form sits inside the checkout page, using Stripe’s Payment Element.
  • Hosted. The shopper is sent to a Stripe Checkout page and comes back after paying.

Cash on delivery works as before, and so do bank transfer and gift cards. Several payment methods can be active at once, each with its own keys.

One answer, four actions

Placing an order (POST /v1/orders) now answers with a paymentAction next to the order. It tells the storefront what to do next, and type is one of four values:

  • none: nothing to do. Cash on delivery and manual transfer answer this.
  • redirect: send the shopper to a hosted payment page at url.
  • form_post: post fields to url.
  • embedded: render the provider form in the page, using clientToken and publicConfig.

The storefront has one handler for all four. A new provider needs no storefront change unless it is embedded. That was the point of the design: the storefront should not know what Stripe is.

The order exists before the shopper pays

If a card payment fails, or the shopper closes the tab, the order stays pending. Two endpoints cover the way back:

  • POST /v1/orders/{id}/payment starts a fresh payment for an unpaid order. It answers 409 when the order can’t be paid any more.
  • POST /v1/orders/{id}/payment/confirm asks the provider for the live status when the shopper returns.

Both need an X-Idempotency-Key, the same rule as placing an order. I wrote about why in idempotent checkout in NestJS. The provider’s webhook is still the source of truth for the paid status. The confirm call only makes the page the shopper sees catch up faster.

Keys live in the admin

The operator enters the Stripe keys on the payment method form in the admin. Secret fields are encrypted at rest with AES-256-GCM, using a key from PAYMENT_CONFIG_ENCRYPTION_KEY. Old keys can stay readable through PAYMENT_CONFIG_ENCRYPTION_KEYS_PREVIOUS, so you can rotate without locking yourself out.

If that key is not set, the app still boots. Saving a secret then answers PAYMENT_CONFIG_ENCRYPTION_UNAVAILABLE, so you find out at the form and not at the first order.

STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET and STRIPE_PUBLISHABLE_KEY still work as a fallback, and the keys entered in the admin win when both exist.

What it still doesn’t do

  • Stripe is the only card provider. If you need another one on launch day, this is not the right store yet. The recipe for adding a provider is written, and the PaymentProvider interface is where it plugs in.
  • It’s one person. I test it, I fix it, and I answer the issues. Weigh that before you put a client’s business on it.
  • Try it in Stripe’s test mode first. Pay with a test card, let one payment fail, and come back to the order, before you turn it on for real. Checkout and orders describes the flow.

The release also adds stable error codes for a few conflicts: CATEGORY_SLUG_TAKEN, CATEGORY_HAS_PRODUCTS, REVIEW_ALREADY_PUBLISHED and QUESTION_ALREADY_PUBLISHED.

What I’d do

Place an order on the storefront demo, then look at the payment method form in the admin demo. To run it yourself, follow the install guide, enter Stripe test keys in the admin and pay with a test card.

The code is on GitHub under MIT. If card checkout breaks for you, open an issue. If you need a provider I haven’t built, tell me which one in the feedback discussion.

Last updated . Every claim about another product in this article links the page it was read on.

// Get started

Run it yourself. Or hand it to Badii.