📖

Stripe Destination Charges

2023/12/24に公開

This post is a part of YAMAP Engineers Advent Calendar 2023.
https://qiita.com/advent-calendar/2023/yamap-engineers

Introduction

Imagine this situation: You've got an existing system with user subscriptions, powered by Stripe for payments. Now, you're introducing a new subsidiary offering additional services through the same system. The question arises: How do you design the payment system for this new venture?
Stripe provides a solution for multiple companies sharing the same customer base called Stripe Connect. There are three types of charges supporting various business models, with Destination Charges being the optimal choice for our situation. In this post, I'll share my experience integrating it.

Creating a payment

Setting up a PaymentIntent with Destination Charge isn't vastly different from a regular PaymentIntent. You just need to include the account where you want to send the money. Here's a quick code snippet in Ruby

Stripe::PaymentIntent.create({
  amount: 1000,
  currency: 'jpy',
  transfer_data: {destination: 'acct_1Nv0FGQ9RKHgCVdK'}, # this line
})

When users make a payment, Stripe generates a Transfer object and an additional Charge object in the destination account, identifiable by an ID starting with py_ instead of the usual ch_.

To access the Charge object of the destination account, you can utilize the Charges API with an extra account parameter:

Stripe::Charge.retrieve(
  'py_something',
  { stripe_account: 'acct_1Nv0FGQ9RKHgCVdK' }
)

Syncing additional information

One challenge I faced was the lack of detailed payment information in the destination account.
I usually associate the payment record id in our system with the metadata parameter of the PaymentIntent, and use the description parameter for payment detail. This can be checked and used as a filter in the main account's dashboard.

Stripe::PaymentIntent.create({
  amount: 1000,
  currency: 'jpy',
  description: 'base fee`,
  metadata: { contract_id: 123 },
  transfer_data: {destination: 'acct_1Nv0FGQ9RKHgCVdK'}, # this line
})

However, these attributes are considered private data of the main account, so Stripe will not automatically transfer the data to the destination account. You have to implement the data sync manually.
I did it by getting the py_something Charge id from the charge.succeeded webhook event, and calling the Charge update API to set descripiton and metadata of the Charge object.

event = Stripe::Webhook.construct_event(
  payload, signature, endpoint_secret
)
charge = event.data.object
transfer = Stripe::Transfer.retrieve(charge.transfer)
Stripe::Charge.update(
  transfer.destination_payment,
  { description: 'base fee', metadata: { contract_id: 123 } },
  stripe_account: 'acct_1Nv0FGQ9RKHgCVdK'
)

Webhooks

Handling webhooks is another crucial aspect. Payments to both the main and subsidiary services trigger the same webhook event in your Stripe account. If you have one controller to handle the main service's webhooks, and another controller to handle the subsidiary's webhooks, they will receive both services' events. To differentiate between the two, you'll need to implement service detection in your controllers.
In my case, I only handle PaymentIntent and Charge events, so it's quite easy. In these events there will be the information about destination account.

// charge.succeeded
{
  "object": {
    "id": "ch_3OQ4bsBl4nMaPGI22r0x9er2",
    "object": "charge",
    "amount": 3000,
    "amount_captured": 3000,
    "amount_refunded": 0,
    ...
    "destination": "acct_1My4cAPnOfYYQzQw",
    ...
    "transfer_data": {
      "amount": null,
      "destination": "acct_1My4cAPnOfYYQzQw"
    },
    "transfer_group": "group_pi_3OQ4bsBl4nMaPGI22Bw37yLK"
  }
}
// payment_intent.succeeded
{
  "object": {
    "id": "pi_3OQ4bsBl4nMaPGI22Bw37yLK",
    "object": "payment_intent",
    "amount": 3000,
    ...
    "transfer_data": {
      "destination": "acct_1My4cAPnOfYYQzQw"
    },
    "transfer_group": "group_pi_3OQ4bsBl4nMaPGI22Bw37yLK"
  }
}

By examining the destination and transfer_data attributes in these events, you can identify whether the event pertains to the main or subsidiary service.

Conclusion

Incorporating Stripe Connect's Destination Charges into our payment system proved to be a seamless process, with some small challenges. I hope this post can be helpful when you face the same situation.

YAMAP テックブログ

Discussion