0

I am using a PayPal SDK in .net (I think it's 'old' version, classic?) I have a bunch of recurring payment agreements under my merchant profile (the ones that can be invoiced manually from https://www.paypal.com/ca/cgi-bin/webscr?cmd=_merchant-hub, and are listed under Activity -> All Reports -> Customer Agreements -> Recurring payments on the PayPal web site). Invoicing them manually works fine, but I'd like to automate that. I am able to get a list of recurring payment profiles to invoice, so I'm just missing the very last step - to actually invoice a recurring payment profile.

I have tried

PayPalAPIInterfaceServiceService.BillUser() 

and

PayPalAPIInterfaceServiceService.BillOutstandingAmount()

and neither of them work. PayPalAPIInterfaceServiceService.BillUser() returns a

Agreement Id is not valid

error (I guess they are looking for a different kind of billing agreement). PayPalAPIInterfaceServiceService.BillOutstandingAmount() returns a

Outstanding balance must be > 0

error. I thought I could perhaps set the outstanding balance on a recurring payment using

PayPalAPIInterfaceServiceService.UpdateRecurringPaymentsProfile()

but, when passing the recurring payment profile ID either in the constructor to UpdateRecurringPaymentsProfileRequestDetailsType or setting through UpdateRecurringPaymentsProfileRequestDetailsType.ProfileID, that resulted in a

Profile ID is not valid for this account. Please resubmit request with the correct profile ID.

When setting UpdateRecurringPaymentsProfileRequestDetailsType.ProfileReference to the recurring payment profile ID, the error message is

The profile ID is invalid

Finally, I have also tried to bill using a reference transaction:

PaymentDetailsType payment = new PaymentDetailsType() { OrderTotal = new BasicAmountType(CurrencyCodeType.USD, amount.ToString()) };
DoReferenceTransactionRequestDetailsType request = new DoReferenceTransactionRequestDetailsType(recurringPaymentId, PaymentActionCodeType.SALE, payment);
var response = service.DoReferenceTransaction(new DoReferenceTransactionReq() { DoReferenceTransactionRequest = new DoReferenceTransactionRequestType(request) });

This results in

Billing Agreement Id or transaction Id is not valid

I'm running out of ideas!

What is the correct PayPal SDK call for invoicing/billing a recurring payment profile, given its profile id?

Jimmy
  • 5,131
  • 9
  • 55
  • 81

1 Answers1

0

As per PayPal merchant technical support, recurring PayPal payment agreements that are created with a link like

https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=###

and have an ID starting with "I-" cannot be invoiced using a PayPal API.

Instead, one has to use reference transactions, which can only be created with the PayPal API (as opposed to a link to a 'hosted button' as shown above).

Now, PayPal merchant accounts don't have Reference Transactions enabled by default; one has to call or email PayPal to get that turned on (some guidance available).

Once reference transactions are enabled (took about two weeks of back & forth with PayPal for mine to be enabled, but maybe I'm less lucky than most :^), one can use the Express Checkout API (among others) to create reference transactions.

Reference transactions are not listed in automated SFTP RPP reports that one can sign up for. They do show up on the merchant hub (and their ID starts with a B- instead of an I-), and one can get the email address associated with the reference transaction from the merchant hub from the tool tip that shows up when hovering the mouse over the listing (the payer email address is not shown when looking at the reference transaction details). One can also get the payer information using GetExpressCheckoutDetails in the code that gets called by the returnUrl, passing it the token that gets passed as a parameter to the returnUrl (same token that is used to call CreateBillingAgreement).

Here's php code that handles the creation of a PayPal reference transaction. One single php file to handle it all. I left some (commented out) debugging code in there to help you walk through this if need be.

<?php
    require_once( dirname(__FILE__) . '/ppconfig.php' ); 

    /* ppconfig.php should contain something like:
    <?php
    global $ppApiUser, $ppApiPwd, $ppApiSig;
    $ppApiUser = '...';
    $ppApiPwd = '...';
    $ppApiSig = '...';
    ?>    
    */

    //print_r($_GET);
   
    if (!array_key_exists("a", $_GET))
        Intro();
    else switch ($_GET["a"])
    { 
        case "go":
            Setup();   
            break;

        case "cf":
            Confirmed();   
            break;

        case "cx":
            Cancelled();   
            break;

        default:
            Intro();
            break;
    }

    function Intro()
    {
        echo("<p>Please <a href='" . BaseUrl() . "?a=go'>click here</a> to create a PayPal billing agreement.</p>");
    }    

    function Setup()
    {
        $post = [
            'PAYMENTREQUEST_0_PAYMENTACTION' => 'AUTHORIZATION',
            'PAYMENTREQUEST_0_AMT' => '0',
            'PAYMENTREQUEST_0_CURRENCYCODE' => 'USD',
            'L_BILLINGTYPE0' => 'MerchantInitiatedBilling',
            'L_BILLINGAGREEMENTDESCRIPTION0' => 'Monthly Fee',
            'cancelUrl' => BaseUrl() . '?a=cx',
            'returnUrl' => BaseUrl() . '?a=cf'
        ];        

        $post = SetupPostArray($post, 'SetExpressCheckout');
        //echo "<p>query: " . http_build_query($post) . "</p>";

        $parsedResponse = DoCurl($post);
        // example response: Array ( [TOKEN] => EC-9WG24287H6582094R [TIMESTAMP] => 2021-05-17T18:14:09Z [CORRELATIONID] => 37010d4454fac [ACK] => Success [VERSION] => 86 [BUILD] => 55627781 )
           
        if ($parsedResponse['ACK'] === "Success")        
            Redirect("https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=" . $parsedResponse['TOKEN']);
        else
            echo("<p>There was an issue creating your PayPal billing agreement; please forward this to us: SetExpressCheckout response = " . print_r($parsedResponse, true) . "</p>");             
    }    

    function Confirmed()
    {
        //$callerIp = $_SERVER['REMOTE_ADDR'];
        //$callerName = gethostbyaddr ( $callerIp );
        //echo("<p>Caller ip: $callerIp  Caller name: $callerName</p>"); returns my own address

        // sample request url: https://xxx.php?a=cf&token=EC-6F958498XP432134J
        // (paypal parameter same as cancel)

        $details = GetExpressCheckoutDetails($_GET["token"]);
        //echo("<p>Payer email: " . $details['EMAIL'] . "  Payer ID: " . $details['PAYERID'] . "</p>");
        
        $post = SetupPostArray([ 'TOKEN' => $_GET["token"] ], 'CreateBillingAgreement');  
        $parsedResponse = DoCurl($post);
        // sample response to create billing agreement: Array ( [BILLINGAGREEMENTID] => B-4EM76674LS64xxxxx [TIMESTAMP] => 2021-05-17T17:51:59Z [CORRELATIONID] => 3be427e93xxxx [ACK] => Success [VERSION] => 86 [BUILD] => 55627781 )

        if ($parsedResponse['ACK'] === "Success")
        {
            echo("<p>You successfully setup a pre-authorized payment (" . $parsedResponse['BILLINGAGREEMENTID'] . "). Thank you.</p>");                               
        }
        else
            echo("<p>There was an issue creating your PayPal billing agreement; please forward this to us: CreateBillingAgreement = " . print_r($parsedResponse, true) . "</p>");                
    }    

    function Cancelled()
    {
        // sample request url: https://xxx.php?a=cx&token=EC-6F958498XP432134J
        // (paypal parameter same as success)

        echo("<p>It looks like you cancelled the creation of your PayPal pre-authorized payment (how could you!) Please <a href='" . BaseUrl() . "?a=go'>click here</a> to try again.</p>");
    }    

    function GetExpressCheckoutDetails($token)
    {
        $post = SetupPostArray([ 'TOKEN' => $token ], 'GetExpressCheckoutDetails');  
        //echo "<p>GetExpressCheckoutDetails query: " . http_build_query($post) . "</p>";
        $parsedResponse = DoCurl($post);
        // sample response to GetExpressCheckoutDetails:  Array ( [TOKEN] => EC-87S04858V0280572C [BILLINGAGREEMENTACCEPTEDSTATUS] => 1 [CHECKOUTSTATUS] => PaymentActionNotInitiated [TIMESTAMP] => 2021-05-19T15:16:06Z [CORRELATIONID] => f5d17498exxxx [ACK] => Success [VERSION] => 86 [BUILD] => 55627781 [EMAIL] => xxxx@gmail.com [PAYERID] => xxxx [PAYERSTATUS] => verified [FIRSTNAME] => xxx [LASTNAME] => xxx [COUNTRYCODE] => xx [SHIPTONAME] => xxx xxx [SHIPTOSTREET] => xxx Dr [SHIPTOCITY] => xxx [SHIPTOSTATE] => xx [SHIPTOZIP] => xxxx [SHIPTOCOUNTRYCODE] => xx [SHIPTOCOUNTRYNAME] => xxx [ADDRESSSTATUS] => Confirmed [CURRENCYCODE] => USD [AMT] => 0.00 [ITEMAMT] => 0.00 [SHIPPINGAMT] => 0.00 [HANDLINGAMT] => 0.00 [TAXAMT] => 0.00 [INSURANCEAMT] => 0.00 [SHIPDISCAMT] => 0.00 [INSURANCEOPTIONOFFERED] => false [PAYMENTREQUEST_0_CURRENCYCODE] => USD [PAYMENTREQUEST_0_AMT] => 0.00 [PAYMENTREQUEST_0_ITEMAMT] => 0.00 [PAYMENTREQUEST_0_SHIPPINGAMT] => 0.00 [PAYMENTREQUEST_0_HANDLINGAMT] => 0.00 [PAYMENTREQUEST_0_TAXAMT] => 0.00 [PAYMENTREQUEST_0_INSURANCEAMT] => 0.00 [PAYMENTREQUEST_0_SHIPDISCAMT] => 0.00 [PAYMENTREQUEST_0_SELLERPAYPALACCOUNTID] => xxx@xxx.com [PAYMENTREQUEST_0_INSURANCEOPTIONOFFERED] => false [PAYMENTREQUEST_0_SHIPTONAME] => xxx xxx [PAYMENTREQUEST_0_SHIPTOSTREET] => xxx Dr [PAYMENTREQUEST_0_SHIPTOCITY] => xxx [PAYMENTREQUEST_0_SHIPTOSTATE] => xx [PAYMENTREQUEST_0_SHIPTOZIP] => xxxxx [PAYMENTREQUEST_0_SHIPTOCOUNTRYCODE] => xx [PAYMENTREQUEST_0_SHIPTOCOUNTRYNAME] => xxx [PAYMENTREQUEST_0_ADDRESSSTATUS] => Confirmed [PAYMENTREQUESTINFO_0_ERRORCODE] => 0 )

        //echo("<p>GetExpressCheckoutDetails response: " . print_r($parsedResponse, true) . "</p>");                

        return $parsedResponse;        
    }
    
    function SetupPostArray($post, $method)
    {
        global $ppApiUser, $ppApiPwd, $ppApiSig; 
        $post['USER'] = $ppApiUser;
        $post['PWD'] = $ppApiPwd;
        $post['SIGNATURE'] = $ppApiSig;
        $post['METHOD'] = $method; 
        $post['VERSION'] = '86';
        return $post;
    }

    function DoCurl($post)
    {
        $ch = curl_init("https://api-3t.paypal.com/nvp");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post));    
        $response = curl_exec($ch);
        curl_close($ch);    

        //echo("<p>Raw response: '$response'</p>");
        // example response: Array ( [TOKEN] => EC-9WG24287H6582094R [TIMESTAMP] => 2021-05-17T18:14:09Z [CORRELATIONID] => 37010d445xxxx [ACK] => Success [VERSION] => 86 [BUILD] => 55627781 )

        parse_str($response, $parsedResponse);

        return $parsedResponse;
    }

    function Redirect($url)
    {
        echo "<script type='text/javascript'> 
        window.location = '$url'; 
        </script>";        
    }

    function BaseUrl()
    {
        //return strtok($_SERVER["REQUEST_URI"], '?'); // see https://stackoverflow.com/a/6975045/68936
        return strtok("https://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]", '?'); // see stackoverflow.com/a/6975045, stackoverflow.com/a/6768831
    }
?>
Jimmy
  • 5,131
  • 9
  • 55
  • 81