Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 11, 2022 01:38 pm GMT

Purchase fulfilment with Checkout, or Wait, what was I paid for?

Imagine youre in the middle of setting up your payments integration. Youve implemented Stripe Checkout, got your webhooks up and running, and even installed a Slack app to tell you when youve made money.

Next up, you need to actually provide the thing or service youre selling to your customers. Not a problem! you think, unaware that youre about to be proven wrong. Youll just add some business logic to your backend when you receive that checkout.session.completed webhook event. You try this out in test mode and get a payload not unlike the following:

{ "object": {  "id": "cs_test_a16Dn1Ja9hTBizgcJ9pWXM5xnRMwivCYDVrT55teciF0mc3vLCUcy6uO99",  "object": "checkout.session",  "after_expiration": null,  "allow_promotion_codes": null,  "amount_subtotal": 3000,  "amount_total": 3000,  "automatic_tax": {   "enabled": false,   "status": null  },  "billing_address_collection": null,  "cancel_url": "https://example.com/cancel",  "client_reference_id": null,  "consent": null,  "consent_collection": null,  "currency": "usd",  "customer": "cus_M5Q7YRXNqZrFtu",  "customer_creation": "always",  "customer_details": {   "address": {    "city": null,    "country": null,    "line1": null,    "line2": null,    "postal_code": null,    "state": null   },   "email": "[email protected]",   "name": null,   "phone": null,   "tax_exempt": "none",   "tax_ids": [   ]  },  "customer_email": null,  "expires_at": 1658319119,  "livemode": false,  "locale": null,  "metadata": {  },  "mode": "payment",  "payment_intent": "pi_3LNFHPGUcADgqoEM2rxLo91k",  "payment_link": null,  "payment_method_options": {  },  "payment_method_types": [   "card"  ],  "payment_status": "paid",  "phone_number_collection": {   "enabled": false  },  "recovered_from": null,  "setup_intent": null,  "shipping": null,  "shipping_address_collection": null,  "shipping_options": [  ],  "shipping_rate": null,  "status": "complete",  "submit_type": null,  "subscription": null,  "success_url": "https://example.com/success",  "total_details": {   "amount_discount": 0,   "amount_shipping": 0,   "amount_tax": 0  },  "url": null }}

From that data you can gather who paid and how much, but what did the user actually buy? How do you know what to ship if you sell physical products or what to provision if your wares are digital?

This is a quirk that trips up a lot of people when they get to this stage. You probably recall providing line_items when creating your Checkout Session, its the field where you specify what exactly the user is purchasing by either providing a Price ID or by creating a Price ad-hoc.

This field isnt included by default when you retrieve a Checkout Session, nor is it in the payload of the webhook event. Instead you need to retrieve the Checkout Session from the Stripe API while expanding the fields that you require. Expanding is the process of requesting additional data or objects from a singular Stripe API call. WIth it you could for example retrieve both a Subscription and the associated Customer object with a single API call rather than two.

Hint: properties that are expandable are noted as such in the API reference. You can learn more about expanding in our video series.

Heres an example using Node and Express on how that would look in your webhook event code:

app.post('/webhook', async (req, res) => {  const sig = req.headers['stripe-signature'];  const endpointSecret = process.env.WEBHOOK_SECRET;  let event;  // Verify the webhook signature  try {    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);  } catch (err) {    console.log(`Webhook error: ${err.message}`);    return res.status(400).send(`Webhook error: ${err.message}`);  }  // Acknowledge that the event was received _before_ doing our business logic  // That way if something goes wrong we won't get any webhook event retries  res.json({ received: true });  switch (event.type) {    case 'checkout.session.completed':      // Retrieve the session, expanding the line items      const session = await stripe.checkout.sessions.retrieve(        event.data.object.id,        {          expand: ['line_items'],        }      );      const items = session.line_items.data.map((item) => {        return item.description;      });      console.log(`        Items purchased: ${items.join(' ')}        Total amount: ${session.amount_total}      `);      break;      }});

The above will retrieve the same Checkout Session, but ask the API to include the full line_items object that youd otherwise not get. We then print the descriptions of each item purchased and the total amount that the customer paid.

This approach might seem obtuse (why not just include the line_items in the payload?), but theres actually a good reason for and benefit to this way of doing things.

Latency

The truth is that it is computationally expensive to retrieve a full list of line items and return them in your webhook event payload. This is especially the case if you have lots of line items for a single Checkout Session. Coupled with the fact that many Stripe users dont use the contents of line_items, adding them in every payload would significantly increase the latency of the webhook event. As such, Stripe has opted for this property to be opt-in to keep the API fast for everybody.

Ensure that you always have the latest up-to-date object

Imagine a scenario where your customer creates a new subscription and youre listening for the following webhook events:

customer.subscription.createdinvoice.createdinvoice.paid

Where in each step you want to perform some business logic before the next step (for example, updating a status in your database).

Then, when you test out your integration you get the events in this order:

invoice.createdinvoice.paidcustomer.subscription.created

Wait what? How can the invoice be created and paid before the subscription is created?

Well, it didnt. The order of webhooks can unfortunately not be trusted due to how the internet works. While the events might be sent in order from Stripe, theres no guarantee that they will be received in order (I blame internet gremlins). This is especially true for events that are generated and sent in quick succession, like the events associated with the creation of a new subscription.

If your business logic relies on these events happening in order, with Stripe objects in varying states, bad stuff can happen. You can mitigate this entirely by always fetching the object in question before making any changes. That way you guarantee that you always have the most up-to-date object which reflects what Stripe has on their end. While this does mean making one extra API call, it also means you never have stale data or suffer from the internet gremlins ire.

Wrap up

These were some tips on doing purchase reconciliation and some webhook best practices. Did I miss anything or do you have follow-up questions? Let me know in the comments below or on Twitter!

About the author

Profile picture of Paul Asjes

Paul Asjes is a Developer Advocate at Stripe where he writes, codes and hosts a monthly Q&A series talking to developers. Outside of work he enjoys brewing beer, making biltong and losing to his son in Mario Kart.


Original Link: https://dev.to/stripe/purchase-fulfilment-with-checkout-or-wait-what-was-i-paid-for-335d

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To