I am using the PayPal REST API to get information about a subscription after the user purchases.
The JS of Paypal suggests that on the onApprove
event, we can for example redirect to a thank you page, and it provides us the Subscription ID.
Thus, one would assume the Subscription is done at this point, and one would assume that calling this REST route https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_transactions would return results. Yet, it does not — initially.
It takes up to 10 minutes for the https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_transactions call to return anything else than an empty JSON string.
My code is pretty simple:
On onApprove
I pass the approved subscription ID in a redirect to a page. When that page loads, I use https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_transactions via PHP cURL to get details of that transaction (buyer email) and check in my database if that email exists. If it exists, I redirect to another page, if not, I stay on this page.
Dead simple, and it works just fine – apart of course that PayPal takes about 10 minutes to actually return the transaction results.
Yes, I could add delays, but this is not the point. The point is that PayPal says the transaction is made, when the onApprove
event happens. Thus, the data must be available in the REST API too. Is this a known issue? What can be done to avoid this delay? I fear the delay is probably arbitrary and might be more than 10 minutes for other users?
Here is the code I use:
JS button approval flow
<div id="paypal-button-container-P-1UU44524AX8090809MMVRJ3Y"></div>
<script src="https://www.paypal.com/sdk/js?client-id=AS-0AbQhD8wSxv0XMvjeRTAUsa-aZtSZm3fSq-qDp_ibhlq9S5XrkgCVDjchICdKS2IZP7IKVo-MTdz7&vault=true&intent=subscription" data-sdk-integration-source="button-factory" data-namespace = "paypal_sdk"></script>
<script>
paypal_sdk.Buttons({
style: {
shape: 'rect',
color: 'white',
layout: 'vertical',
label: 'subscribe'
},
createSubscription: function(data, actions) {
return actions.subscription.create({
/* Creates the subscription */
plan_id: 'P-1UU44524AX8090809MMVRJ3Y'
});
},
onApprove: function(data, actions) {
window.location.replace("https://www.my-site.com/create-account/?subscription_id=" + data.subscriptionID);
}
}).render('#paypal-button-container-P-1UU44524AX8090809MMVRJ3Y'); // Renders the PayPal button
</script>
Server-side process when loading https://www.my-site.com/create-account/?subscription_id=" + data.subscriptionID
<?php
if ( isset( $_GET['subscription_id'] )
&& ! empty( $_GET['subscription_id'] )
&& is_page( 'create-account' )
) {
/**
* Get Access Token
*/
$ch_auth = curl_init();
curl_setopt($ch_auth, CURLOPT_URL, 'https://api-m.sandbox.paypal.com/v1/oauth2/token');
curl_setopt($ch_auth, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch_auth, CURLOPT_POST, 1);
curl_setopt($ch_auth, CURLOPT_POSTFIELDS, "grant_type=client_credentials");
curl_setopt($ch_auth, CURLOPT_USERPWD, 'USR' . ':' . 'PWD');
$headers_auth = array();
$headers_auth[] = 'Content-Type: application/x-www-form-urlencoded';
curl_setopt($ch_auth, CURLOPT_HTTPHEADER, $headers_auth);
$result_auth = curl_exec($ch_auth);
if (curl_errno($ch_auth)) {
echo 'Error:' . curl_error($ch_auth);
}
curl_close($ch_auth);
$auth_arr = json_decode($result_auth);
$auth = $auth_arr->access_token;
/**
* Get Subscription details
*/
$ch_sub = curl_init();
curl_setopt($ch_sub, CURLOPT_URL, 'https://api-m.sandbox.paypal.com/v1/billing/subscriptions/'.$_GET['subscription_id'].'/transactions?start_time=2022-01-21T07:50:20.940Z&end_time=2022-09-24T07:50:20.940Z');
curl_setopt($ch_sub, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch_sub, CURLOPT_CUSTOMREQUEST, 'GET');
$headers_sub = array();
$headers_sub[] = 'Content-Type: application/json';
$headers_sub[] = 'Authorization: Bearer ' . $auth;
curl_setopt($ch_sub, CURLOPT_HTTPHEADER, $headers_sub);
$result_sub = curl_exec($ch_sub);
if (curl_errno($ch_sub)) {
echo 'Error:' . curl_error($ch_sub);
}
curl_close($ch_sub);
$first = end(json_decode($result_sub)->transactions)->payer_name->given_name;
$last = end(json_decode($result_sub)->transactions)->payer_name->surname;
$mail = end(json_decode($result_sub)->transactions)->payer_email;
$exists = email_exists( $mail );
if ( $exists ) {
header('Location: '.'https://www.my-site.com/account/?subscription_id=' . $_GET['subscription_id'] . '&account=' . $exists);
die();
}
}
This always fails until I reload the page something between once and 100 times (it varies)
2
Answers
I don’t know exactly what you are doing, but I recently worked with a frontend application that used a PayPal drop-in form and subscribed to its relative events to trigger calls to our backend.
The calls would take several seconds at max.
Maybe have a look at your event subscribers and make sure they are subscribed to the correct events?
You could incorporate an optimistic redirect and collect the REST reference via a webhook to have it update your existing record once it’s ready on their end.
Although I’m not really familiar with the flow you are using, and I have not heard about these delays, you can trust that the transaction is (being) processed if PayPal returns you with an approved event.
Don’t use list transactions, use the get subscription details API to confirm the status of a subscription after approval.
To log all transactions, implement webhooks for the event PAYMENT.SALE.COMPLETED. This is the only webhook you need to listen to for subscriptions, it will record every transaction made and when it does you can update your "good until" date for the subscription to 1 month in the future or whatever.
To aid in reconciliation, add a unique
custom_id
value during subscription creation (alongside the plan_id) that correlates with the user (in your system) who subscribed. This value will be returned in all future webhooks for the subscription, and can be referenced if for whatever reason you don’t have a record of which user a subscription ID (I-xxxxxxxxxxxx) belongs to