Payment Gateway API

Restrict Content Pro includes a complete payment gateway API that permits developers to integrate Restrict Content Pro with additional payment processors.

Building a payment gateway includes two primary components:

  1. Registering the gateway
  2. Providing an extension of the RCP_Payment_Gateway class

Registering A Gateway

Payment gateways are registered through the  rcp_payment_gateways filter. This receives one parameter, $gateways, that is an array of all currently registered gateways.

Each gateway in the array is an array with the following keys:

  • label - The payment method label shown to customers
  • admin_label - The label used to describe the gateway in the settings screen
  • class - The name of the class that extensions the RCP_Payment_Gateway base class

The ID of the gateway is determined by the key in the main array.

Registering a gateway looks like this:

  <?php
  function pw_rcp_register_2checkout_gateway( $gateways ) {
   
  $gateways['2checkout'] = array(
  'label' => '2Checkout',
  'admin_label' => '2Checkout Redirect',
  'class' => 'RCP_Payment_Gateway_2Checkout'
  );
   
  return $gateways;
  }
  add_filter( 'rcp_payment_gateways', 'pw_rcp_register_2checkout_gateway' );

Once you have registered the gateway, you will write out the gateway class. The class provided should be an extension of the base payment gateway class, which looks like this:

  <?php
  /**
  * Payment Gateway Base Class
  *
  * You can extend this class to add support for a custom gateway.
  * @link http://docs.restrictcontentpro.com/article/1695-payment-gateway-api
  *
  * @package Restrict Content Pro
  * @subpackage Classes/Gateway
  * @copyright Copyright (c) 2017, Pippin Williamson
  * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
  * @since 2.1
  */
   
  class RCP_Payment_Gateway {
   
  /**
  * Array of features the gateway supports, including:
  * one-time (one time payments)
  * recurring (recurring payments)
  * fees (setup fees)
  * trial (free trials)
  *
  * @var array
  * @access public
  */
  public $supports = array();
   
  /**
  * The customer's email address
  *
  * @var string
  * @access public
  */
  public $email;
   
  /**
  * The customer's user account ID
  *
  * @var int
  * @access public
  */
  public $user_id;
   
  /**
  * The customer's username
  *
  * @var string
  * @access public
  */
  public $user_name;
   
  /**
  * The selected currency code (i.e. "USD")
  *
  * @var string
  * @access public
  */
  public $currency;
   
  /**
  * Recurring subscription amount
  * This excludes any one-time fees or one-time discounts.
  *
  * @var int|float
  */
  public $amount;
   
  /**
  * Initial payment amount
  * This is the amount to be billed for the first payment, including
  * any one-time setup fees or one-time discounts.
  *
  * @var int|float
  * @access public
  */
  public $initial_amount;
   
  /**
  * Total discounts applied to the payment
  *
  * @var int|float
  * @access public
  */
  public $discount;
   
  /**
  * Subscription duration
  *
  * @var int
  * @access public
  */
  public $length;
   
  /**
  * Subscription unit: day, month, or year
  *
  * @var string
  * @access public
  */
  public $length_unit;
   
  /**
  * Signup fees to apply to the first payment
  * (This number is included in $initial_amount)
  *
  * @var int|float
  * @access public
  */
  public $signup_fee;
   
  /**
  * Subscription key
  *
  * @var string
  * @access public
  */
  public $subscription_key;
   
  /**
  * Subscription ID number the customer is signing up for
  *
  * @var int
  * @access public
  */
  public $subscription_id;
   
  /**
  * Name of the subscription the customer is signing up for
  *
  * @var string
  * @access public
  */
  public $subscription_name;
   
  /**
  * Whether or not this registration is for a recurring subscription
  *
  * @var bool
  * @access public
  */
  public $auto_renew;
   
  /**
  * URL to redirect the customer to after a successful registration
  *
  * @var string
  * @access public
  */
  public $return_url;
   
  /**
  * Whether or not the site is in sandbox mode
  *
  * @var bool
  * @access public
  */
  public $test_mode;
   
  /**
  * Array of all subscription data that's been passed to the gateway
  *
  * @var array
  * @access public
  */
  public $subscription_data;
   
  /**
  * Webhook event ID (for example: the Stripe event ID)
  * This may not always be populated
  *
  * @var string
  * @access public
  */
  public $webhook_event_id;
   
  /**
  * Payment object for this transaction. Going into the gateway it's been
  * create with the status 'pending' and will need to be updated after
  * a successful payment.
  *
  * @var object
  * @access public
  * @since 2.9
  */
  public $payment;
   
  /**
  * Customer object for this user.
  *
  * @var RCP_Customer
  * @access public
  * @since 3.0
  */
  public $customer;
   
  /**
  * Membership object for this payment.
  *
  * @var RCP_Membership
  * @access public
  * @since 3.0
  */
  public $membership;
   
  /**
  * Start date of the subscription in MySQL format. It starts today by default (empty string).
  *
  * @var string
  * @access public
  */
  public $subscription_start_date;
   
  /**
  * Used for saving an error message that occurs during registration.
  *
  * @var string
  * @access public
  * @since 2.9
  */
  public $error_message;
   
  /**
  * RCP_Payment_Gateway constructor.
  *
  * @param array $subscription_data Subscription data passed from rcp_process_registration()
  *
  * @access public
  * @return void
  */
  public function __construct( $subscription_data = array() ) {
   
  $this->test_mode = rcp_is_sandbox();
  $this->init();
   
  if( ! empty( $subscription_data ) ) {
   
  /**
  * @var RCP_Payments $rcp_payments_db
  */
  global $rcp_payments_db;
   
  $this->email = $subscription_data['user_email'];
  $this->user_id = $subscription_data['user_id'];
  $this->user_name = $subscription_data['user_name'];
  $this->currency = $subscription_data['currency'];
  $this->amount = round( $subscription_data['recurring_price'], 2 );
  $this->initial_amount = round( $subscription_data['price'] + $subscription_data['fee'], 2 );
  $this->discount = $subscription_data['discount'];
  $this->discount_code = $subscription_data['discount_code'];
  $this->length = $subscription_data['length'];
  $this->length_unit = $subscription_data['length_unit'];
  $this->signup_fee = $this->supports( 'fees' ) ? $subscription_data['fee'] : 0;
  $this->subscription_key = $subscription_data['key'];
  $this->subscription_id = $subscription_data['subscription_id'];
  $this->subscription_name = $subscription_data['subscription_name'];
  $this->auto_renew = $this->supports( 'recurring' ) ? $subscription_data['auto_renew'] : false;;
  $this->return_url = $subscription_data['return_url'];
  $this->subscription_data = $subscription_data;
  $this->payment = $rcp_payments_db->get_payment( $subscription_data['payment_id'] );
  $this->customer = $subscription_data['customer'];
  $this->membership = rcp_get_membership( $subscription_data['membership_id'] );
  $this->subscription_start_date = $subscription_data['subscription_start_date'];
   
  if ( $this->is_trial() ) {
  $this->initial_amount = 0;
  }
   
  rcp_log( sprintf( 'Registration for user #%d sent to gateway. Level ID: %d; Initial Amount: %.2f; Recurring Amount: %.2f; Auto Renew: %s; Trial: %s; Subscription Start: %s; Membership ID: %d', $this->user_id, $this->subscription_id, $this->initial_amount, $this->amount, var_export( $this->auto_renew, true ), var_export( $this->is_trial(), true ), $this->subscription_start_date, $this->membership->get_id() ) );
   
  }
   
  }
   
  /**
  * Initialize the gateway configuration
  *
  * This is used to populate the $supports property, setup any API keys, and set the API endpoint.
  *
  * @access public
  * @return void
  */
  public function init() {
   
  /* Example:
   
  $this->supports[] = 'one-time';
  $this->supports[] = 'recurring';
  $this->supports[] = 'fees';
  $this->supports[] = 'trial';
   
  global $rcp_options;
   
  if ( $this->test_mode ) {
  $this->api_endpoint = 'https://sandbox.gateway.com';
  $this->api_key = $rcp_options['my_sandbox_api_key'];
  } else {
  $this->api_endpoint = 'https://live.gateway.com';
  $this->api_key = $rcp_options['my_live_api_key'];
  }
   
  */
   
  }
   
  /**
  * Process registration
  *
  * This is where you process the actual payment. If non-recurring, you'll want to use
  * the $this->initial_amount value. If recurring, you'll want to use $this->initial_amount
  * for the first payment and $this->amount for the recurring amount.
  *
  * After a successful payment, redirect to $this->return_url.
  *
  * @access public
  * @return void
  */
  public function process_signup() {}
   
  /**
  * Process webhooks
  *
  * Listen for webhooks and take appropriate action to insert payments, renew the member's
  * account, or cancel the membership.
  *
  * @access public
  * @return void
  */
  public function process_webhooks() {}
   
  /**
  * Use this space to enqueue any extra JavaScript files.
  *
  * @access public
  * @return void
  */
  public function scripts() {}
   
  /**
  * Load any extra fields on the registration form
  *
  * @access public
  * @return string
  */
  public function fields() {
   
  /* Example for loading the credit card fields :
   
  ob_start();
  rcp_get_template_part( 'card-form' );
  return ob_get_clean();
   
  */
   
  }
   
  /**
  * Validate registration form fields
  *
  * @access public
  * @return void
  */
  public function validate_fields() {
   
  /* Example :
   
  if ( empty( $_POST['rcp_card_cvc'] ) ) {
  rcp_errors()->add( 'missing_card_code', __( 'The security code you have entered is invalid', 'rcp' ), 'register' );
  }
   
  */
   
  }
   
  /**
  * Check if the gateway supports a given feature
  *
  * @param string $item
  *
  * @access public
  * @return bool
  */
  public function supports( $item = '' ) {
  return in_array( $item, $this->supports );
  }
   
  /**
  * Generate a transaction ID
  *
  * Used in the manual payments gateway.
  *
  * @return string
  */
  public function generate_transaction_id() {
  $auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
  return strtolower( md5( $this->subscription_key . date( 'Y-m-d H:i:s' ) . $auth_key . uniqid( 'rcp', true ) ) );
  }
   
  /**
  * Activate or renew the membership. If the membership has been billed `0` times then it is activated for the
  * first time. Otherwise it is renewed.
  *
  * @param bool $recurring Whether or not it's a recurring subscription.
  * @param string $status Status to set the member to, usually 'active'.
  *
  * @access public
  * @return void
  */
  public function renew_member( $recurring = false, $status = 'active' ) {
  if ( 0 == $this->membership->get_times_billed() ) {
  $this->membership->activate();
  } else {
  $this->membership->renew( $recurring, $status );
  }
  }
   
  /**
  * Add error to the registration form
  *
  * @param string $code Error code.
  * @param string $message Error message to display.
  *
  * @access public
  * @return void
  */
  public function add_error( $code = '', $message = '' ) {
  rcp_errors()->add( $code, $message, 'register' );
  }
   
  /**
  * Determines if the subscription is eligible for a trial.
  *
  * @since 2.7
  * @return bool True if the subscription is eligible for a trial, false if not.
  */
  public function is_trial() {
  return ! empty( $this->subscription_data['trial_eligible'] )
  && ! empty( $this->subscription_data['trial_duration'] )
  && ! empty( $this->subscription_data['trial_duration_unit'] )
  ;
  }
   
  }

There are several foundational methods provided by the base class:

  • init() - (required) This is used to set up a few gateway properties, such as API keys and endpoints.
  • process_signup() - (required) This is the method that is run when the registration form is submitted and all validation passes. This is used to actually process the payment and/or set up the gateway subscription.
  • process_webhooks() - (optional) This is the method that is used to detect webhooks from the payment processor. This is usually used for processing renewals.
  • scripts() - (optional) This method allows you to enqueue or output any JS/CSS needed for the payment gateway.
  • fields() - (optional) This method allows you to output gateway-specific fields on the registration form.
  • validate_fields() - (optional) This method allows you to validate the custom fields you have added to the registration form for the gateway.

A gateway is only required to provide the init() and process_signup() methods, but the other methods will likely be used as well for many payment gateways.

Initializing The Class

The init() method is primarily used for declaring which features the gateway supports and checking for a sandbox mode. A common init() method looks like this:

  <?php
  /**
  * Get things going
  *
  * @access public
  * @since 1.0
  * @return void
  */
  public function init() {
  global $rcp_options;
   
  $this->supports[] = 'one-time';
  $this->supports[] = 'recurring';
  $this->supports[] = 'fees';
  $this->supports[] = 'trial';
   
  if ( $this->test_mode ) {
  $this->api_login_id = isset( $rcp_options['authorize_test_api_login'] ) ? sanitize_text_field( $rcp_options['authorize_test_api_login'] ) : '';
  $this->transaction_key = isset( $rcp_options['authorize_test_txn_key'] ) ? sanitize_text_field( $rcp_options['authorize_test_txn_key'] ) : '';
  $this->transaction_signature = isset( $rcp_options['authorize_test_signature_key'] ) ? sanitize_text_field( $rcp_options['authorize_test_signature_key'] ) : '';
  } else {
  $this->api_login_id = isset( $rcp_options['authorize_api_login'] ) ? sanitize_text_field( $rcp_options['authorize_api_login'] ) : '';
  $this->transaction_key = isset( $rcp_options['authorize_txn_key'] ) ? sanitize_text_field( $rcp_options['authorize_txn_key'] ) : '';
  $this->transaction_signature = isset( $rcp_options['authorize_signature_key'] ) ? sanitize_text_field( $rcp_options['authorize_signature_key'] ) : '';
  }
   
  require_once RCP_ANET_PATH . 'vendor/autoload.php';
   
  }

In this particular example, support is declared for one-time payments, recurring payments, signup fees, and free trials. The API keys for both test and live modes are loaded into the class properties, and the gateway SDK file is included.

Processing Signup

The process_signup() method is where most of the magic happens. This is the method that handles calling the payment processor's API or redirecting it to an external payment page. For this example, let's assume we are going to redirect to an external payment page.

Before adding our processing logic to process_signup(), our class looks something like this:

  <?php
  /**
  * Authorize.net Payment Gateway
  *
  * @package rcp-authorize-net
  * @copyright Copyright (c) 2019, Restrict Content Pro team
  * @license GPL2+
  * @since 1.0
  */
   
  namespace RCP\Anet;
   
  use DateTime;
  use Exception;
   
  if ( ! defined( 'ABSPATH' ) ) {
  exit;
  }
   
  class Payment_Gateway extends \RCP_Payment_Gateway {
   
  /**
  * @since 1.0
  * @var string
  * @access private
  */
  private $api_login_id;
   
  /**
  * @since 1.0
  * @var string
  * @access private
  */
  private $transaction_key;
   
  /**
  * @since 1.0
  * @var string
  * @access private
  */
  private $transaction_signature;
   
  /**
  * Get things going
  *
  * @access public
  * @since 1.0
  * @return void
  */
  public function init() {
  global $rcp_options;
   
  $this->supports[] = 'one-time';
  $this->supports[] = 'recurring';
  $this->supports[] = 'fees';
  $this->supports[] = 'trial';
   
  if ( $this->test_mode ) {
  $this->api_login_id = isset( $rcp_options['authorize_test_api_login'] ) ? sanitize_text_field( $rcp_options['authorize_test_api_login'] ) : '';
  $this->transaction_key = isset( $rcp_options['authorize_test_txn_key'] ) ? sanitize_text_field( $rcp_options['authorize_test_txn_key'] ) : '';
  $this->transaction_signature = isset( $rcp_options['authorize_test_signature_key'] ) ? sanitize_text_field( $rcp_options['authorize_test_signature_key'] ) : '';
  } else {
  $this->api_login_id = isset( $rcp_options['authorize_api_login'] ) ? sanitize_text_field( $rcp_options['authorize_api_login'] ) : '';
  $this->transaction_key = isset( $rcp_options['authorize_txn_key'] ) ? sanitize_text_field( $rcp_options['authorize_txn_key'] ) : '';
  $this->transaction_signature = isset( $rcp_options['authorize_signature_key'] ) ? sanitize_text_field( $rcp_options['authorize_signature_key'] ) : '';
  }
   
  require_once RCP_ANET_PATH . 'vendor/autoload.php';
   
  }
   
  /**
  * Process registration
  *
  * @access public
  * @since 1.0
  * @return void
  */
  public function process_signup() {
   
  }
   
  }

We now write our redirection logic in process_signup():

  <?php
  /**
  * Authorize.net Payment Gateway
  *
  * @package rcp-authorize-net
  * @copyright Copyright (c) 2019, Restrict Content Pro team
  * @license GPL2+
  * @since 1.0
  */
   
  namespace RCP\Anet;
   
  use DateTime;
  use Exception;
   
  if ( ! defined( 'ABSPATH' ) ) {
  exit;
  }
   
  class Payment_Gateway extends \RCP_Payment_Gateway {
   
  /**
  * @since 1.0
  * @var string
  * @access private
  */
  private $api_login_id;
   
  /**
  * @since 1.0
  * @var string
  * @access private
  */
  private $transaction_key;
   
  /**
  * @since 1.0
  * @var string
  * @access private
  */
  private $transaction_signature;
   
  /**
  * Get things going
  *
  * @access public
  * @since 1.0
  * @return void
  */
  public function init() {
  global $rcp_options;
   
  $this->supports[] = 'one-time';
  $this->supports[] = 'recurring';
  $this->supports[] = 'fees';
  $this->supports[] = 'trial';
   
  if ( $this->test_mode ) {
  $this->api_login_id = isset( $rcp_options['authorize_test_api_login'] ) ? sanitize_text_field( $rcp_options['authorize_test_api_login'] ) : '';
  $this->transaction_key = isset( $rcp_options['authorize_test_txn_key'] ) ? sanitize_text_field( $rcp_options['authorize_test_txn_key'] ) : '';
  $this->transaction_signature = isset( $rcp_options['authorize_test_signature_key'] ) ? sanitize_text_field( $rcp_options['authorize_test_signature_key'] ) : '';
  } else {
  $this->api_login_id = isset( $rcp_options['authorize_api_login'] ) ? sanitize_text_field( $rcp_options['authorize_api_login'] ) : '';
  $this->transaction_key = isset( $rcp_options['authorize_txn_key'] ) ? sanitize_text_field( $rcp_options['authorize_txn_key'] ) : '';
  $this->transaction_signature = isset( $rcp_options['authorize_signature_key'] ) ? sanitize_text_field( $rcp_options['authorize_signature_key'] ) : '';
  }
   
  require_once RCP_ANET_PATH . 'vendor/autoload.php';
   
  }
   
  /**
  * Process registration
  *
  * @access public
  * @since 1.0
  * @return void
  */
  public function process_signup() {
   
  // Set up the query args
  $args = array(
  'price' => $this->initial_amount, // Includes initial price + signup fees + discounts. Just the base membership level price is $this->amount.
  'description' => $this->subscription_name, // Name of the membership level.
  'custom' => $this->membership->get_id(), // Store the membership ID as a unique identifier.
  'email' => $this->email, // User's email address.
  'return' => $this->return_url // Registration success URL.
  );
   
  // Set up auto renew subscription if applicable.
  if( $this->auto_renew ) {
  $args['interval'] = $this->length_unit; // month, day, year
  $args['interval_count'] = $this->length; // 1, 2, 3, 4 . . .
  }
   
  // Redirect to the external payment page
  wp_redirect( add_query_arg( $args, 'http://paymentpage.com/api/' ) );
  exit;
   
  }
   
  }

Here's a list of important things to do in process_signup():

  1. If $this->auto_renew is FALSE - process a one-time transaction using $this->initial_amount.
  2. If $this->auto_renew is TRUE - set up a recurring subscription at the payment gateway. The amount charged today should be $this->initial_amount. The amount charged on renewals should be $this->amount. Once the subscription is set up, store the gateway's subscription ID in the membership using:
    $this->membership->set_gateway_subscription_id( 'gateway_subscription_id_goes_here' );
    	
  3. Once the initial payment is confirmed as paid, update the pending payment record to set the 'transaction_id' and change the status to 'complete' like so:
    $rcp_payments_db->update( $this->payment->id, array(
    	'transaction_id' => 'your_transaction_id_here', // @todo set transaction ID
    	'status'         => 'complete'
    ) );
    	

Here's a list of useful properties in the base RCP_Payment_Gateway class that you may use:

  • $supports - Array of supported features.
  • $email - Customer's email address.
  • $user_id - Customer's user account ID.
  • $user_name  - Customer's username.
  • $currency - The selected currency code (i.e. "USD").
  • $amount - Recurring subscription amount. This excludes any one-time fees or one-time discounts.
  • $initial_amount - This is the amount to be billed for the first payment. It includes any one-time setup fees, one-time discounts, and credits.
  • $discount - Total amount discounted from the payment.
  • $length - Membership duration.
  • $length_unit - Membership duration unit (day, month, or year).
  • $signup_fee - Signup fees to apply to the first payment. This number is included in $initial_amount.
  • $subscription_key - Unique subscription key for this membership.
  • $subscription_id - The membership level ID number the customer is signing up for.
  • $subscription_name - The name of the membership level the customer is signing up for.
  • $auto_renew - Whether or not this registration is for a recurring subscription.
  • $return_url - URL to redirect the customer to after a successful registration.
  • $test_mode - Whether or not the site is in sandbox mode.
  • $payment - Payment object for this transaction. Going into the gateway this has the status "pending". It should be updated to "complete" after a successful payment is made.
  • $customer - RCP_Customer object for this user.
  • $membership - RCP_Membership object for the pending membership record.
  • $subscription_start_date - Start date of the subscription in MySQL format. It starts today by default (empty string). If a customer's first payment is free (such as for a free trial or 100% off one-time discount) then this value will be set to the date the first payment should be made.

Processing the signup is a major part of building a payment gateway, but there are several other components as well, which we will continue below.

Adding Registration Fields

If your gateway needs to add fields to the registration form, you can use the fields() method. Restrict Content Pro will automatically append the return value for this method to the registration form when the gateway is selected.

For example, many payment gateways need to add a credit/debit card form to the registration form. If you need to do that, this example will be helpful:

public function fields() {
	ob_start();
	rcp_get_template_part( 'card-form' );
	return ob_get_clean();
}

You can read about  rcp_get_template_part() here. "card-form" is a template file provided by Restrict Content Pro that includes a standard credit/debit card template.

If you need any other fields, simply add your HTML to the fields() method.

Note: this method should be used for adding general fields to the registration form, this is for gateway-specific fields only.

Validating Fields

If you need to validate fields that you have added to the registration form, you can use the validate_fields() method to do this.

When validating a field, you will use the add_error() method to register an error message that is then displayed on the registration form.

public function validate_fields() {
	if( empty( $_POST['rcp_card_number'] ) ) {
		$this->add_error( 'missing_card_number', __( 'The card number you have entered is invalid', 'rcp' ) );
	}
	if( empty( $_POST['rcp_card_cvc'] ) ) {
		$this->add_error( 'missing_card_code', __( 'The security code you have entered is invalid', 'rcp' ) );
	}
	if( empty( $_POST['rcp_card_zip'] ) ) {
		$this->add_error( 'missing_card_zip', __( 'The zip / postal code you have entered is invalid', 'rcp' ) );
	}
	if( empty( $_POST['rcp_card_name'] ) ) {
		$this->add_error( 'missing_card_name', __( 'The card holder name you have entered is invalid', 'rcp' ) );
	}
	if( empty( $_POST['rcp_card_exp_month'] ) ) {
		$this->add_error( 'missing_card_exp_month', __( 'The card expiration month you have entered is invalid', 'rcp' ) );
	}
	if( empty( $_POST['rcp_card_exp_year'] ) ) {
		$this->add_error( 'missing_card_exp_year', __( 'The card expiration year you have entered is invalid', 'rcp' ) );
	}
}

These error messages will be displayed on the registration form and will prevent the form from being submitted.

Processing Webhooks

Webhooks are an integral part of payment processor integrations. They are what allow you to detect when a subscription payment has been made or a customer's subscription has been modified in some way from the merchant account.

There are two parts to processing webhooks:

  1. Telling your payment processor the URL to which webhooks should be sent
  2. Processing the webhooks in Restrict Content Pro after they are sent by the payment processor

Instructing the payment process where to send the webhook is different for every merchant processor, but often involves passing a URL to the parameters included in the API request when a subscription is created.

For example, PayPal uses a parameter called "notify_url", so we will use that.

We can include the webhook URL in the API call during process_signup():

public function process_signup() {
	// Set up the query args
	$args = array(
		'price'        => $this->initial_amount, // Includes initial price + signup fees + discounts. Just the base membership level price is $this->amount.
		'description'  => $this->subscription_name, // Name of the membership level.
		'custom'       => $this->membership->get_id(), // Store the membership ID as a unique identifier.
		'email'        => $this->email, // User's email address.
		'return'       => $this->return_url // Registration success URL.
		'notify_url'   => add_query_arg( 'listener', 'my_gateway_id', home_url( 'index.php' ) )
	);
	if( $this->auto_renew ) {
		$args['interval']       = $this->length_unit; // month, day, year
		$args['interval_count'] = $this->length; // 1, 2, 3, 4 . . . 
	}
	// Redirect to the external payment page
	wp_redirect( add_query_arg( $args, 'http://paymentpage.com/api/' ) );
	exit;
}

The URL is nothing more than the site's home URL with a special query arg attached to it. This query arg allows us to tell the difference between a webhook and a standard page request on the site.

Processing the webhooks is simple. All you need to do is place the logic for processing the request from your payment processor in the process_webhooks() method, like this:

public function process_webhooks() {
	// Determine if this is a webhook
	if( ! isset( $_GET['listener'] ) || strtoupper( $_GET['listener'] ) != 'my_gateway_id' ) {
		return;
	}
	// Now retrieve the webhook data from the $_POST super global
}

During the webhook, you will need to perform data validation and sanitization and then also make changes to the customer's account as appropriate. Here are common actions performs during a webhook:

  • Activating an account
  • Renewing a subscription
  • Recording a payment
  • Canceling a subscription

For these actions, you need to know how to modify memberships and how to create membership payments, both of which are described below in their appropriate sections.

To assist you in building your webhook processor, here is an example of how it works in PayPal Express:

  <?php
  /**
  * Process PayPal IPN
  *
  * @access public
  * @since 2.1
  * @return void
  */
  public function process_webhooks() {
   
  if( ! isset( $_GET['listener'] ) || strtoupper( $_GET['listener'] ) != 'EIPN' ) {
  return;
  }
   
  rcp_log( 'Starting to process PayPal Express IPN.' );
   
  $user_id = 0;
  $posted = apply_filters('rcp_ipn_post', $_POST ); // allow $_POST to be modified
  $membership = false;
  $custom = ! empty( $posted['custom'] ) ? explode( '|', $posted['custom'] ) : false;
   
  if( ! empty( $posted['recurring_payment_id'] ) ) {
  $membership = rcp_get_membership_by( 'gateway_subscription_id', $posted['recurring_payment_id'] );
  }
   
  if( empty( $membership ) && ! empty( $custom[1] ) ) {
  $membership = rcp_get_membership( absint( $custom[1] ) );
  }
   
  if( empty( $membership ) || ! $membership->get_id() > 0 ) {
  rcp_log( 'Exiting PayPal Express IPN - membership ID not found.', true );
   
  die( 'no membership found' );
  }
   
  rcp_log( sprintf( 'Processing IPN for membership #%d.', $membership->get_id() ) );
   
  if ( empty( $user_id ) ) {
  $user_id = $membership->get_customer()->get_user_id();
  }
   
  $member = new RCP_Member( $membership->get_customer()->get_user_id() ); // for backwards compatibility
   
  $membership_level_id = $membership->get_object_id();
   
  if( ! $membership_level_id ) {
  rcp_log( 'Exiting PayPal Express IPN - no membership level ID.', true );
   
  die( 'no membership level found' );
  }
   
  if( ! $membership_level = rcp_get_subscription_details( $membership_level_id ) ) {
  rcp_log( 'Exiting PayPal Express IPN - no membership level found.', true );
   
  die( 'no membership level found' );
  }
   
  $amount = isset( $posted['mc_gross'] ) ? number_format( (float) $posted['mc_gross'], 2, '.', '' ) : false;
   
  $membership_gateway = $membership->get_gateway();
   
  // setup the payment info in an array for storage
  $payment_data = array(
  'subscription' => $membership_level->name,
  'payment_type' => $posted['txn_type'],
  'subscription_key' => $membership->get_subscription_key(),
  'user_id' => $user_id,
  'customer_id' => $membership->get_customer()->get_id(),
  'membership_id' => $membership->get_id(),
  'status' => 'complete',
  'gateway' => ! empty( $membership_gateway ) && 'paypal_pro' == $membership_gateway ? 'paypal_pro' : 'paypal_express'
  );
   
  if ( false !== $amount ) {
  $payment_data['amount'] = $amount;
  }
   
  if ( ! empty( $posted['payment_date'] ) ) {
  $payment_data['date'] = date( 'Y-m-d H:i:s', strtotime( $posted['payment_date'] ) );
  }
   
  if ( ! empty( $posted['txn_id'] ) ) {
  $payment_data['transaction_id'] = sanitize_text_field( $posted['txn_id'] );
  }
   
  do_action( 'rcp_valid_ipn', $payment_data, $user_id, $posted );
   
  /* now process the kind of subscription/payment */
   
  $rcp_payments = new RCP_Payments();
  $pending_payment_id = rcp_get_membership_meta( $membership->get_id(), 'pending_payment_id', true );
   
  // Subscriptions
  switch ( $posted['txn_type'] ) :
   
  case "recurring_payment_profile_created":
   
  rcp_log( 'Processing PayPal Express recurring_payment_profile_created IPN.' );
   
  if ( isset( $posted['initial_payment_txn_id'] ) ) {
  $transaction_id = ( 'Completed' == $posted['initial_payment_status'] ) ? $posted['initial_payment_txn_id'] : '';
  } else {
  $transaction_id = $posted['ipn_track_id'];
  }
   
  if ( empty( $transaction_id ) || $rcp_payments->payment_exists( $transaction_id ) ) {
  rcp_log( sprintf( 'Breaking out of PayPal Express IPN recurring_payment_profile_created. Transaction ID not given or payment already exists. TXN ID: %s', $transaction_id ), true );
   
  break;
  }
   
  // setup the payment info in an array for storage
  $payment_data['date'] = date( 'Y-m-d H:i:s', strtotime( $posted['time_created'] ) );
  $payment_data['amount'] = number_format( (float) $posted['initial_payment_amount'], 2, '.', '' );
  $payment_data['transaction_id'] = sanitize_text_field( $transaction_id );
   
  if ( ! empty( $pending_payment_id ) ) {
   
  $payment_id = $pending_payment_id;
   
  // This activates the membership.
  $rcp_payments->update( $pending_payment_id, $payment_data );
   
  } else {
   
  $payment_data['subtotal'] = $payment_data['amount'];
   
  $payment_id = $rcp_payments->insert( $payment_data );
   
  $expiration = date( 'Y-m-d 23:59:59', strtotime( $posted['next_payment_date'] ) );
  $membership->renew( $membership->is_recurring(), 'active', $expiration );
   
  }
   
  do_action( 'rcp_webhook_recurring_payment_profile_created', $member, $this );
  do_action( 'rcp_gateway_payment_processed', $member, $payment_id, $this );
   
  break;
  case "recurring_payment" :
   
  rcp_log( 'Processing PayPal Express recurring_payment IPN.' );
   
  // when a user makes a recurring payment
  update_user_meta( $user_id, 'rcp_paypal_subscriber', $posted['payer_id'] );
   
  $membership->set_gateway_subscription_id( $posted['recurring_payment_id'] );
   
  if ( 'failed' == strtolower( $posted['payment_status'] ) ) {
   
  // Recurring payment failed.
  $membership->add_note( sprintf( __( 'Transaction ID %s failed in PayPal.', 'rcp' ), $posted['txn_id'] ) );
   
  die( 'Subscription payment failed' );
   
  } elseif ( 'pending' == strtolower( $posted['payment_status'] ) ) {
   
  // Recurring payment pending (such as echeck).
  $pending_reason = ! empty( $posted['pending_reason'] ) ? $posted['pending_reason'] : __( 'unknown', 'rcp' );
  $membership->add_note( sprintf( __( 'Transaction ID %s is pending in PayPal for reason: %s', 'rcp' ), $posted['txn_id'], $pending_reason ) );
   
  die( 'Subscription payment pending' );
   
  }
   
  // Recurring payment succeeded.
   
  $membership->renew( true );
   
  $payment_data['transaction_type'] = 'renewal';
   
  // record this payment in the database
  $payment_id = $rcp_payments->insert( $payment_data );
   
  do_action( 'rcp_ipn_subscr_payment', $user_id );
  do_action( 'rcp_webhook_recurring_payment_processed', $member, $payment_id, $this );
  do_action( 'rcp_gateway_payment_processed', $member, $payment_id, $this );
   
  die( 'successful recurring_payment' );
   
  break;
   
  case "recurring_payment_profile_cancel" :
   
  rcp_log( 'Processing PayPal Express recurring_payment_profile_cancel IPN.' );
   
  if( ! $member->just_upgraded() ) {
   
  if( isset( $posted['initial_payment_status'] ) && 'Failed' == $posted['initial_payment_status'] ) {
  // Initial payment failed, so set the user back to pending.
  $membership->set_status( 'pending' );
  $membership->add_note( __( 'Initial payment failed in PayPal Express.', 'rcp' ) );
   
  $this->error_message = __( 'Initial payment failed.', 'rcp' );
  do_action( 'rcp_registration_failed', $this );
  do_action( 'rcp_paypal_express_initial_payment_failed', $member, $posted, $this );
  } else {
  // If this is a completed payment plan, we can skip any cancellation actions. This is handled in renewals.
  if ( $membership->has_payment_plan() && $membership->at_maximum_renewals() ) {
  rcp_log( sprintf( 'Membership #%d has completed its payment plan - not cancelling.', $membership->get_id() ) );
  die( 'membership payment plan completed' );
  }
   
  // user is marked as cancelled but retains access until end of term
  $membership->cancel();
  $membership->add_note( __( 'Membership cancelled via PayPal Express IPN.', 'rcp' ) );
   
  // set the use to no longer be recurring
  delete_user_meta( $user_id, 'rcp_paypal_subscriber' );
   
  do_action( 'rcp_ipn_subscr_cancel', $user_id );
  do_action( 'rcp_webhook_cancel', $member, $this );
  }
   
  die( 'successful recurring_payment_profile_cancel' );
   
  }
   
  break;
   
  case "recurring_payment_failed" :
  case "recurring_payment_suspended_due_to_max_failed_payment" :
   
  rcp_log( 'Processing PayPal Express recurring_payment_failed or recurring_payment_suspended_due_to_max_failed_payment IPN.' );
   
  if( 'cancelled' !== $membership->get_status() ) {
   
  $membership->set_status( 'expired' );
   
  }
   
  if ( ! empty( $posted['txn_id'] ) ) {
   
  $this->webhook_event_id = sanitize_text_field( $posted['txn_id'] );
   
  } elseif ( ! empty( $posted['ipn_track_id'] ) ) {
   
  $this->webhook_event_id = sanitize_text_field( $posted['ipn_track_id'] );
  }
   
  do_action( 'rcp_ipn_subscr_failed' );
   
  do_action( 'rcp_recurring_payment_failed', $member, $this );
   
  die( 'successful recurring_payment_failed or recurring_payment_suspended_due_to_max_failed_payment' );
   
  break;
   
  case "web_accept" :
   
  rcp_log( sprintf( 'Processing PayPal Express web_accept IPN. Payment status: %s', $posted['payment_status'] ) );
   
  switch ( strtolower( $posted['payment_status'] ) ) :
   
  case 'completed' :
   
  if ( empty( $payment_data['transaction_id'] ) || $rcp_payments->payment_exists( $payment_data['transaction_id'] ) ) {
  rcp_log( sprintf( 'Not inserting PayPal Express web_accept payment. Transaction ID not given or payment already exists. TXN ID: %s', $payment_data['transaction_id'] ), true );
  } else {
  $rcp_payments->insert( $payment_data );
  }
   
  // Member was already activated.
   
  break;
   
  case 'denied' :
  case 'expired' :
  case 'failed' :
  case 'voided' :
  if ( $membership->is_active() ) {
  $membership->cancel();
  } else {
  rcp_log( sprintf( 'Membership #%d is not active - not cancelling account.', $membership->get_id() ) );
  }
  break;
   
  endswitch;
   
   
  die( 'successful web_accept' );
   
  break;
   
  endswitch;
   
  }

Modifying Memberships

Since building a payment gateway means manipulating user accounts (for cancellations, activations, payments, etc), it is important that you know how to adjust a member's subscription.

Restrict Content Pro includes a class called RCP_Membership that will help you significantly in this area.

When using the class, you first need to instantiate it, like this:

$membership = rcp_get_membership( $membership_id );

If you have a gateway subscription ID then you can also retrieve a membership by that instead of by the membership ID. Here's an example:

$membership = rcp_get_membership_by( 'gateway_subscription_id', 'your_gateway_subscription_id' );

Once instantiated, you can call any of the public methods available.

For example, when you need to renew a membership (such as when a payment is processed), you will do this:

$membership->renew( true );

The true parameter refers to whether the account is recurring. If the member has a recurring subscription, pass true, otherwise, pass false.

The renew() method will automatically calculate and set the expiration date based on the membership level.

When you need to cancel a membership, you can use the cancel() method:

$membership->cancel();

Activating a membership for the first time is usually done by completing the pending payment. The process of changing a pending payment's status to "complete" automatically activates the associated membership. This handles all the heavy lifting for you. It will be covered in the next section.

For additional examples, see the RCP_Membership class documentation.

Recording Payments

Payments need to be recorded from the payment gateways in order to be displayed in the Restrict > Payments menu. Payments are usually recording during signup (for initial payments) and during webhook processing (for recurring subscription payments).

The RCP_Payments class will be helpful for creating payment records in Restrict Content Pro.

Creating a payment looks like this:

$membership   = rcp_get_membership( $membership_id );
$payments = new RCP_Payments();
$args = array(
	'date'             => date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ), // Date the payment was made
	'subscription'     => $membership->get_membership_level_name(), // Name of the membership level
	'object_id'        => $membership->get_object_id(), // Membership level ID
	'payment_type'     => 'Credit Card', // Payment type. This is mostly for backwards compatibility.
	'gateway'          => 'my_gateway_id', // Gateway slug/ID
	'subscription_key' => $membership->get_subscription_key(), // Subscription key
	'amount'           => $amount, // Total amount
	'subtotal'         => $subtotal, // Subtotal
	'credits'          => $credits, // Credits applied
	'fees'             => $fees, // Signup fees
	'discount_amount'  => $discount_amount, // Amount discounted
	'discount_code'    => $discount_code, // Discount code used
	'user_id'          => $membership->get_customer()->get_user_id(), // ID of the user who made the payment
	'customer_id'      => $membership->get_customer()->get_id(), // ID of the customer
	'membership_id'    => $membership->get_id(), // ID of the membership
	'transaction_type' => 'new', // Type of transaction: either new, upgrade, downgrade, or renewal
	'transaction_id'   => $transaction_id, // Gateway transaction ID
	'status'           => 'complete', // Payment status - complete, pending, failed
);
$payments->insert( $args );

It is up to you to specify each of the necessary parameters. These are usually obtained from the POST data in the webhook or from the API response in the signup processing.

Completing a pending payment can be done once you have the relevant RCP_Membership object. The pending payment ID is stored with that record, so you need the membership object first. Once you have it, it can be completed like so:

$payments = new RCP_Payments();
$pending_payment_id = rcp_get_membership_meta( $membership->get_id(), 'pending_payment_id', true );

if ( ! empty( $pending_payment_id ) ) {
	$payment_data = array( 'status' => 'complete' ); // Array of data to update
	$payments->update( absint( $pending_payment_id ), $payment_data );
}

When a pending payment is completed, the corresponding membership is automatically activated. The customer is granted access and will be sent the Active Membership Email (if configured).

Complete gateway examples

To better assist you in creating your own payment gateway, we have two complete examples:

  1. The official Authorize.net payment gateway. This gateway is in a separate add-on so it's a good example of how to create a full standalone gateway in a separate plugin.
  2. A more abstracted example of how to use the API with fully commented code. These two files are also complete plugins, but they are not for a real payment gateway and are just examples of how to use the API.
Have more questions? Submit a request
Powered by Zendesk