phpstripe-paymentscheckout

With Stripe API, How To Get session_id of Checkout Session That Created a payment_intent Object, From payment_intent.succeeded Webhook


I am working on developing and testing a Stripe integration, written in PHP, and it's working beautifully. I can create sessions, redirect to the checkout form, and when the payments are complete it sends an event to my webhhook script, which successfully processes the information about the payment going through.

When I create a session, I store the data about the form filled out on my site, in a database, and when the payment goes through, I store information in a different table, which is great.

The problem I'm having is that I don't know how to link up the information about the successful payment, with the session that generated it.

Linking up these data is essential to me, because I want to track which sessions actually result in successful payments, so that I can analyze the flow of the user interface and track conversion rates and analyze factors that lead to abandonment of the checkout session.

In some cases, it is easy to link these things up. For example, if there's only one session generated and one successful payment, associated with a given email in a given time-frame, I can just link them up. The problem is that I want to be able to deal with the (likely common) scenario where a person creates multiple sessions and abandons them. I cannot link the payment to the most recent session associated with the email, in this scenario, because it's possible that a single customer would create two sessions, but complete the payment on the first, earlier-created session.

I can't figure out how to access the session_id from the payment_intent object that is returned to my webhook. Some thoughts I have had about how to possibly approach this include:

I would like to follow "best practices" here, but it's not clear to me how Stripe intends people to link up or access the data, or if this is perhaps an oversight on their end.

If you give me example code I would prefer seeing it in PHP if possible but you don't need to show me any code at all; just giving me an abstract or general idea of how to accomplish this would be sufficient and I could come up with the coding details on my own.


Solution

  • The way Stripe's data is structured is that the Session object has a field payment_intent which contains the id of a payment_intent object. Initially, when the session is created, this field is empty or null, but when the payment is carried out, it becomes populated. The payment_intent object does not contain any relevant fields to link them, in part because a payment_intent can be generated through many different methods, not just a checkout session. So I need to use the Session object, but I need to access it after the session is completed.

    Thus I was able to achieve the desired result by setting up a webhook to listen to the checkout.session.completed event, as by the time this event fires, that field is populated and refers to a payment that has successfully gone through, and that event returns the session object, which contains the relevant field. A webhook for the payment_intent.succeeded event is less useful because it only returns the payment_intent object from which it is impossible to access the relevant info without an additional API call. The solution by @hmunoz provides a way to do this, but I prefer my solution because it avoids this additional API call. Because API calls rely on network access, they are often the slowest step in my script, and the most error-prone, so I tend to prefer a solution which minimizes them.

    Once I've retrieved Stripe's ID referring to the payment_intent object corresponding to the session, I then update the entry in my database corresponding to the session, to contain Stripe's payment_intent ID, and it becomes possible to join the data in my databases.

    I preferred this solution to using metadata because it does not necessitate passing any metadata. There may be a way to use metadata to achieve a similar result, using my own internally-generated ID's, but I was not able to get it to work because it seemed Stripe was just discarding the metadata I passed. Perhaps I wasn't doing something right, but I got this solution working before I was able to get a metadata-based one working.

    Notes on ID's: Switching to Local ID's is Beneficial But Tricky

    One tiny point of caution or note, because I have a webhook that processes the payment_intent.succeeded event (because it can be generated through things other than checkout sessions) and use that to enter some details from the payment_intent object, including its ID, into a local database, you do not have a guarantee about in which order the events will fire. Thus, if you are looking to link up the tables in your local database that correspond to the checkout session objects, and the payment_intent objects, you might end up saving a payment_intent's ID in your table for sessions, before the corresponding row for that payment_intent has been entered into the table for payment_intents.

    As such I needed to write a condition into the event handling for each events, which checks for whether that row exists or not (which corresponds to which event was handled first.) If checkout.session.completed fires first, I put Stripe's payment_intent's ID in the entry for the session and then, when payment_intent.succeeded fires, I not only add the payment_intent object, but I update the row for the session with my local ID for the payment_intent, which is an integer, allowing for more rapid and computationally-less-intense joins.

    If, on the other hand, payment_intent.success fires first, then when checkout.session.completed fires, I retrieve the appropriate local ID in my database, referring to the completed payment, and put that into the local table for the session.

    I strongly recommend doing this, even though it is more work, because Stripe's ID's are long character strings of somewhat arbitrary length (i.e. they reserve the right to lengthen them, according to their documentation) and this means you need a pretty long index on a text field for good performance in joins, something I want to avoid. Converting to your own local integer ID's for each table allows you to join them locally with better performance and less load on your server. Event processing is relatively rare relative to the times you will join this data, both because of customers viewing their own data, and if you're like me and want to look at and analyze your data a lot.