Spring 2020

Taking An Auction House Cashless

With national lockdowns being enforced in England due to COVID-19, like many other businesses, Biddle & Webb were faced with a huge problem.

As a regional auction house, the business depended on operating their weekly auctions in order to survive. While they already offered online realtime bidding, there were a number of other issues lockdown posed:

  • Many lots tend to sell to bidders in the room
  • Customers couldn't randomly turn up and collect their lots
  • Online payments were not supported

It goes without saying, that last issue was a big one... and one we'd have to tackle first and as fast as possible.

Staggering collections is another story which I might write up, but essentially it resulted in a microsite which allowed customers to pick an arrival slot and be texted a unique code to present during collection.

Landscape

Fortunately, Biddle & Webb operated their own proprietary auction management system and website. Largely built with ColdFusion and MySQL, this system allowed staff to manage things such as:

  • Sales and lots
  • Customers
  • Consignments
  • Invoices

Previously, when a customer had won items at an auction, they would need to either phone up or visit the on-site cash office in order to settle their invoice balance and arrange collection of their lots. The staff would then manually go into the system and mark the users invoice as paid.

As there was already a digital paper trail of both customers and invoices, a bunch of the leg work was in place - we just had to automate the last step.

Stripe Checkout

I already knew I was going to go with Stripe as our payment processor. More specifically, I was interested in their hosted checkout. We didn't have much time, definitely not enough to build out our own checkout flow to reliably handle the volume we expected. One thing to keep in mind is that an auction houses' customer base are primarily of a certain age (think of a number and add 20), so usability and compatibility had to be a priority.

Directing the user to Stripe Checkout was a piece of cake. Using the website, users could already see a list of their invoices. We simply needed to add a button to unpaid invoices and direct the user to Stripe Checkout to take payment. Luckily Stripe allows you to create a checkout session via a simple POST request, which kept my exposure to writing any ColdFusion to a bear minimum:

<cfhttp method="post" url="https://api.stripe.com/v1/checkout/sessions">
	<cfhttpparam
		type="HEADER"
		name="Authorization"
		value="Bearer #STRIPE_API_KEY#"
	/>
	<cfhttpparam type="FORMFIELD" name="customer_email" value="#USER_EMAIL#" />
	<cfhttpparam type="FORMFIELD" name="payment_method_types[]" value="card" />
	<cfhttpparam type="FORMFIELD" name="line_items[][name]" value="#SALE_NAME#" />
	<cfhttpparam
		type="FORMFIELD"
		name="line_items[][description]"
		value="#INVOICE_NUMBER#"
	/>
	<cfhttpparam
		type="FORMFIELD"
		name="line_items[][amount]"
		value="#INVOICE_TOTAL#"
	/>
	<cfhttpparam
		type="FORMFIELD"
		name="client_reference_id"
		value="#CUSTOMER_ID#"
	/>
	<cfhttpparam
		type="FORMFIELD"
		name="payment_intent_data[metadata][invoice_id]"
		value="#INVOICE_ID#"
	/>
	<cfhttpparam
		type="FORMFIELD"
		name="payment_intent_data[metadata][sale_id]"
		value="#SALE_ID#"
	/>
	...
</cfhttp>

Along with adding a link to pay via the user's account page, we also sent out emails to request payment once a sale had finished to all users with outstanding balances.

Stripe Webhooks

Now users could actually make a payment, we just needed to automate the invoice reconciliation. To do this, we'd need to tap into Stripe Webhooks. Me and ColdFusion shared a quick glance and we both knew deep down neither of us fancied it. I spun up a quick Next.js project and added an API endpoint to accept Stripe webhook requests.

As the Stripe account was exclusively used for accepting one off payments for invoices, I could afford to be fairly lax when it came to processing webhooks events. I simply wanted to check for payment_intent.succeeded events, determine if we had a corresponding customer and invoice reference in the metadata and update the invoice status accordingly.

export default async (req, res) => {
	const {
		query,
		method,
		body: { type, data },
	} = req
 
	switch (method) {
		case "POST":
			switch (type) {
				case "payment_intent.succeeded":
					// Parse Stripe object
					const paymentIntent = data.object
					const invoiceId = paymentIntent.metadata.invoice_id
 
					// Bail if no invoice ID
					if (!invoiceId) {
						res.status(422).end()
						break
					}
 
					// Mark invoice as paid
					await markInvoicePaid(invoiceId)
 
					break
				default:
					// Unexpected event type
					res.status(400).end()
			}
 
			// Return a response to acknowledge receipt of the event
			res.status(200).end()
			break
		default:
			res.setHeader("Allow", ["POST"])
			res.status(405).end(`Method ${method} Not Allowed`)
	}
}

After adding payment confirmation and failure email notifications, it was ready to test and launch.

Launch

Roughly 3 weeks since we decided to go cashless, Biddle & Webb held it's first remote sale.

Working with Biddle & Webb on a range of projects over the years, namely their realtime bidding system and live audio visual feed, I was no stranger to the sense of impending doom that sale days could induce. It's fast paced and it's live. If something goes wrong, you can't just stop the sale. You have to think on your feet.

Fortunately the actual sale went smoothly. The real time bidding system was fairly tried and tested at this point and was not the part I was worried about. My anxiety was reserved for the moment we sent out the invoice payment reminders post-sale, staring at the Stripe dashboard praying for a sea of green.

I'm afraid there's no exciting twist. Despite the rare dispute, the Stripe solution went on to receive an exceedingly high payment success rate and processed just shy of £2m in its first year and £3.5m to date.

Stripe FiguresStripe Overview


It was one of those projects where it went way smoother than I could have ever imagined and required next to no maintenance once launched. It's simply a testament to Stripe's hosted checkout and developer experience.

Yes, we could have improved the Stripe integration, such as itemising lots on the checkout page, but it served it's purpose and kept the business alive.

While they did eventually re-open their cash office - most customers still opted to pay online, which I think speaks to its ease of use.