Search code examples
paypalpaypal-sandbox

PayPal List Transactions API for subscriptions takes too long to respond with new subscriptions' data


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)


Solution

    1. Don't use list transactions, use the get subscription details API to confirm the status of a subscription after approval.

    2. 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.

    3. 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