How To Setup Multiple IPN Receivers in PayPal

Instant Payment Notifications (IPN) allow your applications to receive notifications from PayPal on payments made. This means that your application can fulfill an order automatically upon receiving such a notification. However, when you get your second application up with its own IPN you suddenly find out that PayPal lets you set only one Notification URL.

Of course there’s the whole notify_url charade and counterparts for all of PayPal’s APIs, but, unfortunately, there are cases when you simply can’t get to set those:

  • Plugins that are hard-coded, where you can’t alter their core and they just force you to set the URL, so you end up locked in; this may be true for all sorts of applications in all sorts of languages (especially true for compiled ones)
  • Third-party billing services like e-Junkie, Kajabi, 1shoppingcart and many others, they just lock you in, and tell you to set the IPN in PayPal
  • Subscriptions and Recurring Payments; yep, PayPal does not allow you to bind a specific IPN for subscriptions at all (let me know if I’m incorrect, but I’ve spent days looking at the manuals)
  • Multiplexing one IPN to two or more sources, for synchronization, custom alerts etc.

For everything else, modify your forms and API calls to include the notify_url attribute.

Multiple Notification URLs in PayPal

The easiest and most straightforward solution would be to get multiple PayPal accounts, right? Right, BUT:

No Multiple Accounts. Should you register for more than one Personal Account, PayPal reserves the right to terminate all of your accounts and will restrict you from the system going forward. Users may register and hold one Personal Account and either one Premier or one Business Account.

…from PayPal’s Terms and Conditions

So unless you have a separate legal business entity (company) for each Business Account), you’re out of luck. However, there a simple and sweet way to overcome this single IPN URL business – receive IPN and broadcast it.

The concept

An single IPN URL is setup for PayPal to notify your application of payments. The IPN handler code broadcasts PayPal’s payload to other IPN URLs you may have selectively or in bulk.

Advantages:

  • multiple PayPal IPN URLs now possible
  • no need to change any IPN code unless it filters requests by IP
  • can centralize IP filtering into the broadcast IPN, which means easier to maintain when PayPal’s IP ranges change
  • can centralize logging
  • can have queuing and rebroadcast if satellites (all your other IPN URLs) are unreachable

Disadvantages:

  • if broadcast IPN goes down – complete blackout (PayPal will notify you, though)

The code

This IPN broadcast code is in PHP, but the concept is non-language specific. Simply port the code and it will do equally well or even better.

<?php
  /*
   * This is a PayPal IPN (Instant Payment Notification) broadcaster
   * Since PayPal does not provide any straightforward way to add
   * multiple IPN listeners we'll have to create a central IPN
   * listener that will broadcast (or filter and dispatch) Instant
   * Payment Notifications to different destinations (IPN listeners)
   *
   * Destination IPN listeners must not panic and recognize IPNs sent
   * by this central broadcast as valid ones in terms of source IP
   * and any other fingerprints. Any IP filtering must add this host,
   * other adjustments made as necessary.
   *
   * IPNs are logged into files for debugging and maintenance purposes
   *
   * this code comes with absolutely no warranty
   * https://codeseekah.com
  */

  ini_set( 'max_execution_time', 0 ); /* Do not abort with timeouts */
  ini_set( 'display_errors', 'Off' ); /* Do not display any errors to anyone */
  $urls = array(); /* The broadcast session queue */

  /* List of IPN listener points */
  $ipns = array(
      'mystore' => 'https://mystore.com/ipn.php',
      'myotherstore' => 'https://mybigstore.com/paypal_ipn.php',
      'myotherandbetterstore' => 'https://slickstore.com/paypal/ipn.php'
    );
    
  /* Fingerprints */

  if ( /* My Store IPN Fingerprint */
    preg_match( '#^\d+\|[a-f0-9]{32}$#', $_POST['custom'] ) /* Custom hash */
    and $_POST['num_cart_items'] == 2 /* alwayst 1 item in cart */
    and strpos( $_POST['item_name1'], 'MySite.com Product' ) == 0 /* First item name */
  ) $urls []= $ipns['mystore']; /* Choose this IPN URL if all conditions have been met */

  if ( /* My Other Store IPN Fingerprint */
    sizeof( explode('_', $_POST['custom']) ) == 7 /* has 7 custom pieces */
  ) $urls []= $ipns['myotherstore']; /* Choose this IPN URL if all conditions have been met */

  /* My Other And Better Store IPN Fingerprint */
  $custom = explode('|', $_POST['custom']);
  if (
    isset($custom[2]) and $custom[2] == 'FROM_OB_STORE' /* custom prefixes */
  ) $urls []= $ipns['myotherandbetterstore']; /* Choose this IPN URL if all conditions have been met */

  /* ... */
  
  
  /* Broadcast */
  
  if ( !sizeof($urls) ) $urls = $ipns; /* No URLs have been matched */
  $urls = array_unique( $urls ); /* Unique, just in case */

  /* Broadcast (excluding IPNs from the list according to filter is possible */
  foreach ( $urls as $url ) broadcast( $url );

  header( 'HTTP/1.1 200 OK', true, 200 );
  exit(); /* Thank you, bye */

  /* Perform a simple cURL-powered proxy request to broadcast */
  function broadcast( $url ) {

    /* Format POST data accordingly */
    $data = array();
    foreach ($_POST as $key => $value) $data []= urlencode($key).'='.urlencode($value);
    $data = implode('&', $data);

    /* Log the broadcast */
    file_put_contents('_logs/'.time().'.'.reverse_lookup( $url ).'-'.rand(1,100), $data);

    $ch = curl_init(); /* Initialize */

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    curl_exec($ch); /* Execute HTTP request */

    curl_close($ch); /* Close */
  }

  function reverse_lookup( $url ) {
    global $ipns;
    foreach ( $ipns as $tag => $_url ) {
      if ( $url == $_url ) return $tag;
    }
    return 'unknown';
  }
?>

Grab the source here: Multiple PayPal IPN Broadcast PHP Script

The script is quite simple:

  1. be ready to receive an IPN from PayPal
  2. construct a list of broadcast targets by looking at the IPN data
  3. broadcast (the script uses cURL as you can see)

You can remove any fingerprint filtering and have this central IPN broadcast to all your URLs regardless, but filtering is a good idea. Nothing beats the custom IPN POST variable, if it’s possible have all your payments go to PayPal with a unique prefix in the custom field and then filter based on that prefix. Refer to the PayPal IPN variables list in order to construct reliable fingerprints.

PayPal Business Accounts can also have up to 8 associated non-primary e-mails, so you can build a fingerprint based on the receiver_email IPN variable instead.

Remember to always verify all IPN requests with PayPal in your satellites or in your broadcast IPN (check PHP PayPal IPN script example).

The conclusion

Thus, multiple PayPal IPN with one PayPal account is absolutely possible and is not too difficult. I’d love to hear what improvements you do to this snippet, so give me a shout anytime.