diff --git a/commerce_paypal.module b/commerce_paypal.module
index 8767833..052eef3 100644
--- a/commerce_paypal.module
+++ b/commerce_paypal.module
@@ -303,6 +303,7 @@ function commerce_paypal_ipn_pending_reason($pending_reason) {
  *   The response array from PayPal if successful or FALSE on error.
  */
 function commerce_paypal_api_request($payment_method, $nvp = array(), $order = NULL) {
+
   // Get the API endpoint URL for the payment method's transaction mode.
   $url = commerce_paypal_api_server_url($payment_method['settings']['server']);
 
@@ -319,7 +320,7 @@ function commerce_paypal_api_request($payment_method, $nvp = array(), $order = N
   drupal_alter('commerce_paypal_api_request', $nvp, $order, $payment_method);
 
   // Log the request if specified.
-  if ($payment_method['settings']['log']['request'] == 'request') {
+  if ($payment_method['settings']['log']['request'] === 'request') {
     // Mask the credit card number and CVV.
     $log_nvp = $nvp;
     $log_nvp['PWD'] = str_repeat('X', strlen($log_nvp['PWD']));
@@ -384,7 +385,7 @@ function commerce_paypal_api_request($payment_method, $nvp = array(), $order = N
   }
 
   // Log the response if specified.
-  if ($payment_method['settings']['log']['response'] == 'response') {
+  if ($payment_method['settings']['log']['response'] === 'response') {
     watchdog('commerce_paypal', 'PayPal server response: !param', array('!param' => '<pre>' . check_plain(print_r($response, TRUE)) . '</pre>', WATCHDOG_DEBUG));
   }
 
@@ -392,7 +393,240 @@ function commerce_paypal_api_request($payment_method, $nvp = array(), $order = N
 }
 
 /**
- * Returns the URL to the specified PayPal API server.
+ * Submits a REST API request to PayPal.
+ *
+ * This function is currently used by PayPal Vault and the Card On File portions
+ * of Website Payments Pro.
+ *
+ * @param $payment_method
+ *   The payment method instance array associated with this API request.
+ * @param array $rest
+ *   Parameters for the REST API call itself: method, endpoint, and version.
+ * @param array $params
+ *   Parameters for this API request. This array is encoded into the JSON
+ *   request object which is set as the data for the cURL request.
+ * @param $order
+ *   The order the payment request is being made for.
+ * @return object A response object similar to that returned by drupal_http_request, with
+ * A response object similar to that returned by drupal_http_request, with
+ * the decoded PayPal API response in the ->data property of the object.
+ */
+function commerce_paypal_rest_api_request($payment_method, $rest = array(), $params = array(), $order = NULL) {
+
+  // Get the API endpoint URL for the payment method's transaction mode.
+  $url = commerce_paypal_rest_api_server_url($payment_method['settings']['server']);
+
+  // Set up our REST request based on the parameters we were sent.
+  $rest['token'] = commerce_paypal_rest_api_token($payment_method, $params);
+  if (!$rest['token']) {
+    $log_params = commerce_paypal_sanitize_rest_api_params($params);
+    watchdog('commerce_paypal', 'PayPal REST API request was unable to retrieve authorization token: !param', array('!param' => '<pre>' . check_plain(print_r(array('Rest API' => $rest,'Params' => $log_params), TRUE)) . '</pre>'), WATCHDOG_ERROR);
+  }
+  // Make sure we have a default method and version.
+  $rest += array(
+    'method' => 'GET',
+    'version' => 'v1',
+  );
+  // If we don't have an endpoint specified, fail out here with watchdog.
+  // For now we're relying on the requester to fill this in every time.
+  if (!isset($rest['endpoint']) || empty($rest['endpoint'])) {
+    $log_params = commerce_paypal_sanitize_rest_api_params($params);
+    watchdog('commerce_paypal', 'PayPal REST API request had no endpoint given: !param', array('!param' => '<pre>' . check_plain(print_r(array('Rest API' => $rest,'Params' => $log_params), TRUE)) . '</pre>'), WATCHDOG_ERROR);
+    return FALSE;
+  }
+
+  // Add version and endpoint to our URL.
+  $url = $url . '/' . $rest['version'] . '/' . $rest['endpoint'];
+
+  // Allow modules to alter parameters of the API request. A different alter
+  // is used here for REST API requests -- the request objects differ greatly
+  // and we wouldn't want any stray Classic API request alters to mess things
+  // up.
+  drupal_alter('commerce_paypal_rest_api_request', $params, $order, $payment_method);
+
+  // Log the request if specified.
+  if ($payment_method['settings']['log']['request'] === 'request') {
+    // Sanitize the request to mask CC number, CVV, and account info.
+
+    // This has been turned into a separate function for potential re-use and
+    // so that REST API sanitization can be added to the same place easily.
+    $log_params = commerce_paypal_sanitize_rest_api_params($params);
+
+    watchdog('commerce_paypal', 'PayPal REST API request to @url: !param', array('@url' => $url, '!param' => '<pre>' . check_plain(print_r(array('Rest API' => $rest,'Params' => $log_params), TRUE)) . '</pre>'), WATCHDOG_DEBUG);
+  }
+
+  // Serialize our params into a JSON request object.
+
+  $rest_json = json_encode($params);
+  $json_error = json_last_error();
+  // Make sure someone didn't pass us a bogus array to json_encode().
+  if (!$rest_json && $json_error !== JSON_ERROR_NONE) {
+    $log_params = commerce_paypal_sanitize_rest_api_params($params);
+    watchdog('commerce_paypal', 'PayPal REST API request failed to json_encode(): !param', array('!param' => '<pre>' . check_plain(print_r(array('Rest API' => $rest,'Params' => $log_params), TRUE)) . '</pre>'), WATCHDOG_ERROR);
+    return FALSE;
+  }
+
+  // Setup the cURL request.
+  $ch = curl_init();
+
+  // Set up the REST API info: JSON request/headers and OAuth token.
+  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $rest['method']);
+  curl_setopt($ch, CURLOPT_POSTFIELDS, $rest_json);
+  curl_setopt($ch, CURLOPT_HEADER, 1);
+  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+    'Content-Type: application/json',
+    'Content-Length: ' . strlen($rest_json),
+    'Authorization: Bearer ' . $rest['token'],
+  ));
+  curl_setopt($ch, CURLOPT_URL, $url);
+  curl_setopt($ch, CURLOPT_VERBOSE, 0);
+  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+  curl_setopt($ch, CURLOPT_NOPROGRESS, 1);
+  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
+  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
+
+  // Commerce PayPal requires SSL peer verification, which may prevent out of
+  // date servers from successfully processing API requests. If you get an error
+  // related to peer verification, you may need to download the CA certificate
+  // bundle file from http://curl.haxx.se/docs/caextract.html, place it in a
+  // safe location on your web server, and update your settings.php to set the
+  // commerce_paypal_cacert variable to contain the absolute path of the file.
+  // Alternately, you may be able to update your php.ini to point to the file
+  // with the curl.cainfo setting.
+  if (variable_get('commerce_paypal_cacert', FALSE)) {
+    curl_setopt($ch, CURLOPT_CAINFO, variable_get('commerce_paypal_cacert', ''));
+  }
+  $result = curl_exec($ch);
+
+  // Log any errors to the watchdog.
+  if ($error = curl_error($ch)) {
+    watchdog('commerce_paypal', 'cURL error: @error', array('@error' => $error), WATCHDOG_ERROR);
+    return FALSE;
+  }
+  curl_close($ch);
+
+  // We need to replicate the header magic from drupal_http_request here.
+  // REST API callers need to expect a similar $response object due to the
+  // need for the HTTP response code in particular.
+  $response = new stdClass();
+  list($headers, $response_body) = preg_split("/\r\n\r\n|\n\n|\r\r/", $result, 2);
+  $headers = preg_split("/\r\n|\n|\r/", $headers);
+  list($protocol, $code, $status_message) = explode(' ', trim(array_shift($headers)), 3);
+  $response->protocol = $protocol;
+  $response->status_message = $status_message;
+  // These are pretty raw in comparison to drupal_http_request but that's ok.
+  $response->headers = $headers;
+  $response->code = $code;
+
+  // Decode the REST API response object.
+  $response->data = json_decode($response_body);
+
+  // Log the response if specified.
+  if ($payment_method['settings']['log']['response'] === 'response') {
+    watchdog('commerce_paypal', 'PayPal server response: !param', array('!param' => '<pre>' . check_plain(print_r($response, TRUE)) . '</pre>', WATCHDOG_DEBUG));
+  }
+
+  return $response;
+}
+
+/**
+ * Get a REST API OAuth2 token. Use a secondary call to generate one if it does not yet exist.
+ */
+function commerce_paypal_rest_api_token($payment_method, $params) {
+  $token_settings = variable_get('commerce_paypal_rest_api_token', array());
+  if (empty($token_settings) || empty($token_settings['access_token']) || $token_settings['expires'] <= (time() + 60)) {
+    // No token yet, or it is expired or close enough to refresh.
+    $token_settings = commerce_paypal_rest_api_token_fetch($payment_method, $params);
+    if (!$token_settings) {
+      // The _fetch function watchdogs its own failure so we just return FALSE here.
+      return FALSE;
+    }
+    variable_set('commerce_paypal_rest_api_token', $token_settings);
+  }
+  // By now we should have a token or we should have returned FALSE earlier.
+  return $token_settings['access_token'];
+}
+
+function commerce_paypal_rest_api_token_fetch($payment_method, $params) {
+  $url = commerce_paypal_rest_api_server_url($payment_method['settings']['server']);
+  $url .= '/v1/oauth2/token';
+
+  $ch = curl_init();
+  curl_setopt($ch, CURLOPT_URL, $url);
+  curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json'));
+  curl_setopt($ch, CURLOPT_USERPWD, $payment_method['settings']['rest_api_clientid'] . ":" . $payment_method['settings']['rest_api_secret']);
+  curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
+  curl_setopt($ch, CURLOPT_POST, 1);
+  curl_setopt($ch, CURLOPT_VERBOSE, 0);
+  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+  curl_setopt($ch, CURLOPT_NOPROGRESS, 1);
+  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
+  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
+  curl_setopt($ch, CURLOPT_FAILONERROR, TRUE);
+
+  if (variable_get('commerce_paypal_cacert', FALSE)) {
+    curl_setopt($ch, CURLOPT_CAINFO, variable_get('commerce_paypal_cacert', ''));
+  }
+  $result = curl_exec($ch);
+  // Log any errors to the watchdog.
+  if ($error = curl_error($ch)) {
+    watchdog('commerce_paypal', 'cURL error: @error', array('@error' => $error), WATCHDOG_ERROR);
+    return FALSE;
+  }
+  curl_close($ch);
+
+  // Give our soon-to-be-serialized-as-a-variable array a real expiration time.
+  $response = json_decode($result, TRUE);
+  $response['expires'] = $response['expires_in'] + time();
+  unset($response['expires_in']);
+
+  return $response;
+}
+
+function commerce_paypal_sanitize_param(&$param, $key) {
+  switch ($key) {
+    case 'number':
+      $param = str_repeat('X', strlen($param) - 4) . substr($param, -4);
+      break;
+    case 'cvv2':
+    case 'cvv':
+      $param = str_repeat('X', strlen($param));
+      break;
+  }
+}
+
+/**
+ * Sanitizes API request parameters for logging to watchdog.
+ *
+ * @param $params
+ *   The request parameters which need to be sanitized to remove credit card number and CVV codes.
+ */
+function commerce_paypal_sanitize_rest_api_params($params) {
+  array_walk_recursive($params, 'commerce_paypal_sanitize_param');
+
+  return $params;
+}
+
+/**
+ * Returns the URL to the specified PayPal REST API server.
+ *
+ * @param $server
+ *   Either sandbox or live indicating which server to get the URL for.
+ *
+ * @return
+ *   The URL to use to submit requests to the PayPal API server.
+ */
+function commerce_paypal_rest_api_server_url($server) {
+  switch ($server) {
+    case 'sandbox':
+      return 'https://api.sandbox.paypal.com';
+    case 'live':
+      return 'https://api.paypal.com';
+  }
+}
+
+/**
+ * Returns the URL to the specified PayPal Classic API (NVP endpoint) server.
  *
  * @param $server
  *   Either sandbox or live indicating which server to get the URL for.
diff --git a/modules/vault/README.txt b/modules/vault/README.txt
new file mode 100644
index 0000000..6ceed48
--- /dev/null
+++ b/modules/vault/README.txt
@@ -0,0 +1,39 @@
+PayPal Vault
+
+Implements PayPal Vault in Drupal Commerce Card On File for PayPal WPP
+
+REQUIREMENTS
+------------
+PayPal Vault requires a REST API App in your PayPal Account, you will
+need to obtain your client id and secret to use this module.
+
+Visit http://developer.paypal.com for more information
+
+This module requires the following modules:
+
+ * commerce_paypal_wpp
+ * commerce_cardonfile
+
+INSTALLATION
+------------
+
+ * Install as you would normally install a contributed Drupal module. See:
+   https://drupal.org/documentation/install/modules-themes/modules-7
+   for further information.
+
+ * Download the certs file:
+   https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt
+
+ * Place the certs file anywhere in your filesystem and modify your settings.php
+   to include the path:
+
+   $conf['commerce_paypal_cacert'] = '/path/to/the/file/ca-bundle.crt';
+
+CONFIGURATION
+-------------
+
+ * Configuration options are on the Payment Method for WPP
+
+ * Enter your client id and secret
+
+ * choose to enable Card on File support
diff --git a/modules/vault/commerce_paypal_vault.info b/modules/vault/commerce_paypal_vault.info
new file mode 100644
index 0000000..ebf188c
--- /dev/null
+++ b/modules/vault/commerce_paypal_vault.info
@@ -0,0 +1,6 @@
+name = PayPal Vault
+description = Implements PayPal Vault in Drupal Commerce Card On File.
+package = Commerce (PayPal)
+dependencies[] = commerce_paypal_wpp
+dependencies[] = commerce_cardonfile
+core = 7.x
diff --git a/modules/vault/commerce_paypal_vault.module b/modules/vault/commerce_paypal_vault.module
new file mode 100644
index 0000000..ef9226e
--- /dev/null
+++ b/modules/vault/commerce_paypal_vault.module
@@ -0,0 +1,454 @@
+<?php
+
+/**
+ * @file
+ * Implements PayPal Vault (via WPP checkout process) in Drupal Commerce Card On File.
+ */
+
+/**
+ * Implements hook_menu().
+ */
+function commerce_paypal_vault_menu() {
+  $items = array();
+
+  // Add a menu item for capturing authorizations.
+  $items['admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/paypal-vault-capture'] = array(
+    'title' => 'Capture',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('commerce_paypal_vault_capture_form', 3, 5),
+    'access callback' => 'commerce_paypal_vault_capture_access',
+    'access arguments' => array(3, 5),
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'context' => MENU_CONTEXT_INLINE,
+    'weight' => 2,
+  );
+
+  return $items;
+}
+
+/**
+ * Determines access to the prior authorization capture form for PayPal WPP
+ * Vault credit card transactions.
+ *
+ * @param $order
+ *   The order the transaction is on.
+ * @param $transaction
+ *   The payment transaction object to be captured.
+ * @return bool TRUE or FALSE indicating capture access.
+ * TRUE or FALSE indicating capture access.
+ */
+function commerce_paypal_vault_capture_access($order, $transaction) {
+  // Return FALSE if the transaction isn't for PayPal or isn't awaiting capture.
+  if ($transaction->payment_method != 'paypal_wpp' || $transaction->remote_status != 'authorize') {
+    return FALSE;
+  }
+
+  // Return FALSE if it is more than 29 days past the original authorization.
+  if (REQUEST_TIME - $transaction->created > 86400 * 29) {
+    return FALSE;
+  }
+
+  // Allow access if the user can update payments on this transaction.
+  return commerce_payment_transaction_access('update', $transaction);
+}
+
+/**
+ * Form callback: allows the user to capture a prior authorization via Paypal
+ * Vault.
+ */
+function commerce_paypal_vault_capture_form($form, &$form_state, $order, $transaction) {
+  form_load_include($form_state, 'inc', 'commerce_paypal_wpp', 'includes/commerce_paypal_wpp.admin');
+  $form = commerce_paypal_wpp_capture_form($form, $form_state, $order, $transaction);
+
+  $form['#validate'] = array('commerce_paypal_wpp_capture_form_validate');
+  $form['#submit'] = array('commerce_paypal_vault_capture_form_submit');
+
+  return $form;
+}
+
+/**
+ * Submit handler: process a prior authorization capture via WPP.
+ */
+function commerce_paypal_vault_capture_form_submit($form, &$form_state) {
+  $transaction = $form_state['transaction'];
+  $amount = $form_state['values']['amount'];
+
+  $order = $form_state['order'];
+  $payment_method = $form_state['payment_method'];
+  $authorization_valid = TRUE;
+
+  // If the original authorization was more than 3 days ago, PayPal's honor
+  // period is over and a reauthorization is required before capturing.
+  if (REQUEST_TIME - $transaction->created > 86400 * 3) {
+    // Submit the re-authorization request.
+    $rest = array(
+      'method' => 'POST',
+      'version' => 'v1',
+      'endpoint' => 'payments/authorization/' . $transaction->remote_id . '/reauthorize',
+    );
+    $params = array(
+      'amount' => array(
+        'total' => commerce_paypal_format_amount($amount),
+        'currency' => $transaction->currency_code,
+      ),
+    );
+    $response = commerce_paypal_rest_api_request($payment_method, $rest, $params, $order);
+
+    // If the response contains an authorization ID...
+    if (isset($response->data->id)) {
+      // Update the original transaction with the new ID to use when capturing.
+      $transaction->remote_id = $response->data->id;
+    }
+    else {
+      // Otherwise do not allow the capture to proceed.
+      $transaction->message .= '<br />' . t('Reauthorization failed: @time', array('@time' => format_date(REQUEST_TIME, 'short')));
+      $authorization_valid = FALSE;
+
+      // Display a failure message on the redirect.
+      drupal_set_message(t('PayPal requires a reauthorization before capture after 3 days have passed since the initial authorization.'), 'error');
+      drupal_set_message(t('Reauthorization failed with the following error, so the transaction could not capture and will remain in a pending status.'), 'error');
+      drupal_set_message(check_plain($response->data->message), 'error');
+    }
+  }
+
+  // If the authorization is valid or successfully reauthorized...
+  if ($authorization_valid) {
+    // Determine the remaining balance if the capture is successful.
+    $balance = commerce_payment_order_balance($order);
+    $balance = commerce_currency_convert($balance['amount'], $balance['currency_code'], $transaction->currency_code);
+    $transaction_balance =  $balance - commerce_currency_decimal_to_amount($amount, $transaction->currency_code);
+
+    // Submit the capture request request to PayPal.
+    $rest = array(
+      'method' => 'POST',
+      'version' => 'v1',
+      'endpoint' => 'payments/authorization/' . $transaction->remote_id . '/capture',
+    );
+    $params = array(
+      'amount' => array(
+        'total' => commerce_paypal_format_amount($amount),
+        'currency' => $transaction->currency_code,
+      ),
+    );
+    if ($transaction_balance == 0) {
+      $params['is_final_capture'] = TRUE;
+    }
+    $response = commerce_paypal_rest_api_request($payment_method, $rest, $params, $order);
+
+    $transaction->payload[REQUEST_TIME . '-capture'] = $response;
+
+    if ($response && $response->code == 200) {
+
+      drupal_set_message(t('Prior authorization captured successfully.'));
+
+      // If this capture did not complete the authorization and subsequent
+      // captures can be processed against it, create a follow-up transaction
+      // to represent the remaining authorization amount.
+      if (!$response->data->is_final_capture) {
+        $new_transaction = clone($transaction);
+        unset($new_transaction->transaction_id, $new_transaction->revision_id);
+        $new_transaction->amount = $transaction_balance;
+        commerce_payment_transaction_save($new_transaction);
+
+        drupal_set_message(t("A follow-up transaction was made to represent the remaining authorization amount that can be captured within PayPal's allowed time limit."));
+      }
+
+      // Update the original transaction amount to the actual capture amount,
+      // its remote ID to the capture's transaction ID, and its statuses to
+      // indicate successful payment.
+      $transaction->amount = commerce_currency_decimal_to_amount($amount, $transaction->currency_code);
+      $transaction->remote_id = $response->data->id;
+      $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
+      $transaction->remote_status = $response->data->state;
+
+      $transaction->message = t('Name: @name. Transaction status: !status.', array(
+        '@name' => 'Card on File',
+        '!status' => $transaction->status,
+      ));
+
+      // Note the capture in the transaction message.
+      $transaction->message .= '<br />' . t('Captured: @date', array('@date' => format_date(REQUEST_TIME, 'short')));
+
+    }
+    else {
+      // Display an error message but leave the transaction pending.
+      drupal_set_message(t('Prior authorization capture failed, so the transaction will remain in a pending status.'), 'error');
+      drupal_set_message(check_plain($response['L_LONGMESSAGE0']), 'error');
+    }
+
+    // Save the updated original transaction.
+    commerce_payment_transaction_save($transaction);
+
+    // Redirect back to the current order payment page.
+    $form_state['redirect'] = 'admin/commerce/orders/' . $form_state['order']->order_id . '/payment';
+  }
+}
+
+/**
+ * Implements hook_commerce_payment_method_info_alter().
+ *
+ * Used to add the PayPal Vault callbacks to PayPal Website Payments Pro.
+ * The rest of the integration has to be done directly by the WPP module.
+ */
+function commerce_paypal_vault_commerce_payment_method_info_alter(&$methods) {
+  // The WPP payment method is the one that accepts credit cards directly,
+  // rather than as a redirected payment method. As such, we hook into it
+  // in order to enable Card On File functionality through its payment method.
+  if (module_exists('commerce_paypal_wpp') && isset($methods['paypal_wpp'])) {
+    $methods['paypal_wpp']['cardonfile'] = array(
+      'create callback' => 'commerce_paypal_vault_cardonfile_create',
+      'update callback' => 'commerce_paypal_vault_cardonfile_update',
+      'delete callback' => 'commerce_paypal_vault_cardonfile_delete',
+      'charge callback' => 'commerce_paypal_vault_cardonfile_charge',
+    );
+  }
+}
+
+/**
+ * Create callback for PayPal Vault stored cards.
+ */
+function commerce_paypal_vault_cardonfile_create($form, $form_state, $payment_method, $card) {
+  $card_number = $form_state['values']['credit_card']['number'];
+  $card_expire_month = $form_state['values']['credit_card']['exp_month'];
+  $card_expire_year = $form_state['values']['credit_card']['exp_year'];
+  $card_type = $form_state['values']['credit_card']['type'];
+  $card_owner = $form_state['values']['credit_card']['owner'];
+  $name_fragments = explode(' ', $card_owner);
+  $last_name = array_pop($name_fragments);
+  $first_name = implode(' ', $name_fragments);
+
+  // Create the params array for the REST API request.
+  $params = array(
+    'payer_id' => 'user_' . $card->uid,
+    'type' => $card_type,
+    'number' => $card_number,
+    'expire_month' => $card_expire_month,
+    'expire_year' => $card_expire_year,
+    'first_name' => $first_name,
+    'last_name' => $last_name,
+  );
+  $rest = array(
+    'endpoint' => 'vault/credit-cards',
+    'method' => 'POST',
+    'version' => 'v1',
+  );
+
+  $response = commerce_paypal_rest_api_request($payment_method, $rest, $params);
+
+  // @todo: This needs much better error handling based on the PayPal response.
+  if ($response && $response->code == '201') {
+    // Op success!
+    $card->remote_id = $response->data->id;
+    $card->card_name = $card_owner;
+    return $card;
+  }
+  else {
+    // Our request failed. Whoops.
+    watchdog('commerce_paypal', 'PayPal Vault card on file creation failed: !param', array('!param' => '<pre>' . check_plain(print_r($response, TRUE)) . '</pre>'), WATCHDOG_ERROR);
+    return FALSE;
+  }
+
+}
+
+/**
+ * Delete callback for PayPal Vault stored cards.
+ */
+function commerce_paypal_vault_cardonfile_delete($form, &$form_state, $payment_method, $card) {
+  // Miraculously, deleting a PayPal Vault card is really easy.
+  // None of the rigmarole from the various CC gateways.
+  $rest = array(
+    'method' => 'DELETE',
+    'endpoint' => 'vault/credit-cards/' . $card->remote_id,
+    'version' => 'v1',
+  );
+
+  $response = commerce_paypal_rest_api_request($payment_method, $rest);
+
+  // PayPal Vault returns a 204 on a successful deletion.
+  if ($response && $response->code == 204) {
+    return TRUE;
+  }
+  else {
+    // Something screwed up here. Watchdog the response.
+    watchdog('commerce_paypal', 'PayPal Vault card on file deletion failed: !param', array('!param' => '<pre>' . check_plain(print_r($response, TRUE)) . '</pre>'), WATCHDOG_ERROR);
+    return FALSE;
+  }
+}
+
+/**
+ * Update callback for PayPal Vault stored cards.
+ * https://developer.paypal.com/docs/api/#update-a-stored-credit-card
+ *
+ * Doesn't work yet -- waiting on feedback from PayPal Developer Support.
+ */
+function commerce_paypal_vault_cardonfile_update($form, &$form_state, $payment_method, $card) {
+  $card_expire_month = $form_state['values']['credit_card']['exp_month'];
+  $card_expire_year = $form_state['values']['credit_card']['exp_year'];
+  $card_owner = $form_state['values']['credit_card']['owner'];
+  $name_fragments = explode(' ', $card_owner);
+  $last_name = array_pop($name_fragments);
+  $first_name = implode(' ', $name_fragments);
+  $params = array(
+    array(
+      'op' => 'replace',
+      'path' => '/first_name',
+      'value' => $first_name,
+    ),
+    array(
+      'op' => 'replace',
+      'path' => '/last_name',
+      'value' => $last_name,
+    ),
+    array(
+      'op' => 'replace',
+      'path' => '/expire_month',
+      'value' => $card_expire_month,
+    ),
+    array(
+      'op' => 'replace',
+      'path' => '/expire_year',
+      'value' => $card_expire_year,
+    ),
+  );
+  $rest = array(
+    'method' => 'PATCH',
+    'endpoint' => 'vault/credit-cards/' . $card->remote_id,
+    'version' => 'v1',
+  );
+
+  $response = commerce_paypal_rest_api_request($payment_method, $rest, $params);
+
+  // PayPal Vault returns a 200 on a successful update.
+  if ($response && $response->code == 200) {
+    return TRUE;
+  }
+  else {
+    // Something screwed up here. Watchdog the response.
+    watchdog('commerce_paypal', 'PayPal Vault card on file update failed: !param', array('!param' => '<pre>' . check_plain(print_r($response, TRUE)) . '</pre>'), WATCHDOG_ERROR);
+    return FALSE;
+  }
+}
+
+/**
+ * Charge callback for PayPal Vault stored cards.
+ *
+ * @param array $payment_method
+ *  The payment method instance definition array.
+ * @param object $card
+ *   The stored credit card data array to be processed
+ * @param object $order
+ *   The order object that is being processed
+ * @param array $charge
+ *   The price array for the charge amount with keys of 'amount' and 'currency'
+ *   If null the total value of the order is used.
+ *
+ * @return
+ *   TRUE if the transaction was successful, FALSE otherwise.
+ *   FALSE will cause the checkout form (if we are involved in one)
+ *   to be rebuilt with the appropriate errors.
+ */
+function commerce_paypal_vault_cardonfile_charge($payment_method, $card, $order, $charge = NULL) {
+
+  if (!isset($charge)) {
+    $wrapper = entity_metadata_wrapper('commerce_order', $order);
+    $charge = commerce_line_items_total($wrapper->commerce_line_items);
+  }
+
+  $amount = commerce_currency_amount_to_decimal($charge['amount'], $charge['currency_code']);
+  $rest = array(
+    'method' => 'POST',
+    'version' => 'v1',
+    'endpoint' => 'payments/payment',
+  );
+  $params = array(
+    'intent' => ($payment_method['settings']['txn_type'] == 'auth_capture' ? 'sale' : 'authorize'),
+    'payer' => array(
+      'payment_method' => 'credit_card',
+      'funding_instruments' => array(
+        array(
+          'credit_card_token' => array(
+            'credit_card_id' => $card->remote_id,
+            'payer_id' => 'user_' . $card->uid,
+          ),
+        ),
+      ),
+    ),
+    'transactions' => array(
+      array(
+        'amount' => array(
+          'total' => commerce_paypal_format_amount($amount),
+          'currency' => $charge['currency_code'],
+        ),
+        'description' => "Order number $order->order_number",
+      ),
+    ),
+  );
+
+  $response = commerce_paypal_rest_api_request($payment_method, $rest, $params, $order);
+
+  // @todo: needs better error checking?
+  $transaction = commerce_payment_transaction_new($payment_method['method_id'], $order->order_id);
+  $transaction->instance_id = $payment_method['instance_id'];
+  $transaction->remote_id = isset($response->data->id) ? $response->data->id : '';
+  $transaction->amount = $charge['amount'];
+  $transaction->currency_code = $charge['currency_code'];
+  $transaction->payload[REQUEST_TIME] = $response;
+  $transaction->remote_status = isset($response->data->state) ? $response->data->state : 'failed';
+  if (isset($response->data->intent) && $response->data->intent == 'authorize' && isset($response->data->state) && $response->data->state = 'approved') {
+    // Set our remote status to authorize so we can capture it later.
+    $transaction->remote_status = 'authorize';
+    if (isset($response->data->transactions[0]->related_resources[0]->authorization->id)) {
+      // Save our authorization id for future use with capture.
+      // @todo: find a way to get this data easily?
+      $transaction->remote_id = $response->data->transactions[0]->related_resources[0]->authorization->id;
+    } else {
+      // Set the transaction to failed as we need the authorization id to
+      // capture this order.
+      $transaction->remote_status = 'failed';
+    }
+  }
+  if (in_array($transaction->remote_status, array('approved', 'created', 'authorize'))) {
+    switch ($payment_method['settings']['txn_type']) {
+      case COMMERCE_CREDIT_AUTH_ONLY:
+        $transaction->status = COMMERCE_PAYMENT_STATUS_PENDING;
+        break;
+      case COMMERCE_CREDIT_AUTH_CAPTURE:
+        $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
+        break;
+      case COMMERCE_CREDIT_CAPTURE_ONLY:
+        $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
+        break;
+    }
+  }
+  else {
+    $transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE;
+  }
+  $transaction->message = t('Name: @name. Transaction status: !status.', array(
+    '@name' => 'Card on File',
+    '!status' => $transaction->status,
+  ));
+
+  commerce_payment_transaction_save($transaction);
+
+  // @todo: Check for other error types and do deactivation of expired/messed-up cards here.
+  if ($transaction->status == COMMERCE_PAYMENT_STATUS_FAILURE) {
+    if (isset($response->data->details)) {
+      $messages = array();
+      foreach ($response->data->details as $detail) {
+        $messages[] = $detail->field . ' -- ' . $detail->issue;
+      }
+      drupal_set_message(t('We received the following error processing your card. Please enter your information again or try a different card.'), 'error');
+      watchdog('commerce_paypal', 'PayPal Vault card on file charge failed: !param', array('!param' => implode('<br/>', array_map('check_plain', $messages))), WATCHDOG_ERROR);
+    }
+    else {
+      drupal_set_message(t('We received an error while processing your card. Please try a different card or contact a site administrator for assistance.'), 'error');
+    }
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+function commerce_paypal_format_amount($amount) {
+  return number_format((float) $amount, 2, '.', '');
+}
\ No newline at end of file
diff --git a/modules/wpp/commerce_paypal_wpp.module b/modules/wpp/commerce_paypal_wpp.module
index 1916792..65a817e 100644
--- a/modules/wpp/commerce_paypal_wpp.module
+++ b/modules/wpp/commerce_paypal_wpp.module
@@ -36,9 +36,8 @@ function commerce_paypal_wpp_menu() {
  *   The order the transaction is on.
  * @param $transaction
  *   The payment transaction object to be captured.
- *
- * @return
- *   TRUE or FALSE indicating capture access.
+ * @return bool TRUE or FALSE indicating capture access.
+ * TRUE or FALSE indicating capture access.
  */
 function commerce_paypal_wpp_capture_access($order, $transaction) {
   // Return FALSE if the transaction isn't for PayPal or isn't awaiting capture.
@@ -83,6 +82,8 @@ function commerce_paypal_wpp_default_settings() {
     'api_username' => '',
     'api_password' => '',
     'api_signature' => '',
+    'rest_api_clientid' => '',
+    'rest_api_secret' => '',
     'server' => 'sandbox',
     'code' => TRUE,
     'card_types' => drupal_map_assoc(array('visa', 'mastercard', 'amex', 'discover')),
@@ -95,6 +96,9 @@ function commerce_paypal_wpp_default_settings() {
 
 /**
  * Payment method callback: settings form.
+ *
+ * @param $settings array
+ * @return array $form settings
  */
 function commerce_paypal_wpp_settings_form($settings = array()) {
   module_load_include('inc', 'commerce_payment', 'includes/commerce_payment.credit_card');
@@ -118,6 +122,16 @@ function commerce_paypal_wpp_settings_form($settings = array()) {
     '#title' => t('Signature'),
     '#default_value' => $settings['api_signature'],
   );
+  $form['rest_api_clientid'] = array(
+    '#type' => 'textfield',
+    '#title' => t('REST API Client ID'),
+    '#default_value' => $settings['rest_api_clientid'],
+  );
+  $form['rest_api_secret'] = array(
+    '#type' => 'textfield',
+    '#title' => t('REST API Secret (Password)'),
+    '#default_value' => $settings['rest_api_secret'],
+  );
   $form['server'] = array(
     '#type' => 'radios',
     '#title' => t('PayPal server'),
@@ -141,6 +155,20 @@ function commerce_paypal_wpp_settings_form($settings = array()) {
     '#description' => t('This should match the similar setting in your PayPal account.'),
     '#default_value' => $settings['code'],
   );
+  if (module_exists('commerce_cardonfile') && module_exists('commerce_paypal_vault')) {
+    $form['cardonfile'] = array(
+      '#title' => t('Enable Card on File functionality with this payment method using PayPal Vault.'),
+      '#type' => 'checkbox',
+      '#default_value' => isset($settings['cardonfile']) ? $settings['cardonfile'] : FALSE,
+    );
+  }
+  else {
+    $form['cardonfile'] = array(
+      '#title' => t('Card on File'),
+      '#type' => 'markup',
+      '#markup' => t('To enable Card on File functionality download and install the Commerce Card on File and Commerce PayPal Vault modules.'),
+    );
+  }
   $form['currency_code'] = array(
     '#type' => 'select',
     '#title' => t('Default currency'),
@@ -173,6 +201,7 @@ function commerce_paypal_wpp_settings_form($settings = array()) {
     ),
     '#default_value' => $settings['log'],
   );
+
   return $form;
 }
 
@@ -214,6 +243,12 @@ function commerce_paypal_wpp_submit_form($payment_method, $pane_values, $checkou
  * Payment method callback: checkout form validation.
  */
 function commerce_paypal_wpp_submit_form_validate($payment_method, $pane_form, $pane_values, $order, $form_parents = array()) {
+  // If the customer specified a card on file, skip the normal validation.
+  if (module_exists('commerce_cardonfile') && !empty($payment_method['settings']['cardonfile']) &&
+    !empty($pane_values['cardonfile']) && $pane_values['cardonfile'] !== 'new') {
+    return;
+  }
+
   module_load_include('inc', 'commerce_payment', 'includes/commerce_payment.credit_card');
 
   // Validate the credit card fields.
@@ -237,6 +272,11 @@ function commerce_paypal_wpp_submit_form_submit($payment_method, $pane_form, $pa
     return FALSE;
   }
 
+  if (module_exists('commerce_cardonfile') && $payment_method['settings']['cardonfile'] &&
+    !empty($pane_values['cardonfile']) && $pane_values['cardonfile'] !== 'new') {
+    return commerce_paypal_wpp_vault_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $charge);
+  }
+
   // Ensure we can determine a valid IPv4 IP address as required by PayPal WPP.
   $ip_address = ip_address();
 
@@ -280,7 +320,7 @@ function commerce_paypal_wpp_submit_form_submit($payment_method, $pane_form, $pa
       $valid_billing_address = FALSE;
     }
 
-    foreach (array('thoroughfare', 'locality', 'postal_code', 'country') as $address_key) {
+    foreach (array('thoroughfare', 'locality', 'country') as $address_key) {
       if (empty($address_value[$address_key])) {
         $valid_billing_address = FALSE;
       }
@@ -448,10 +488,84 @@ function commerce_paypal_wpp_submit_form_submit($payment_method, $pane_form, $pa
   commerce_payment_transaction_save($transaction);
 
   // If the payment failed, display an error and rebuild the form.
+
   if (!in_array($response['ACK'], array('SuccessWithWarning', 'Success'))) {
     drupal_set_message(t('We encountered an error processing your payment. Please verify your credit card details or try a different card.'), 'error');
     return FALSE;
   }
+  elseif (   module_exists('commerce_cardonfile')
+          && !empty($payment_method['settings']['cardonfile'])
+          && !empty($pane_values['credit_card']['cardonfile_store'])
+          && $pane_values['credit_card']['cardonfile_store']
+          ) {
+    // Check if we need to store a new card on file. If yes, then do so here.
+    // The card on file module's "create" callback is not invoked; we need
+    // to do this part ourselves.
+
+    $card_number = $pane_values['credit_card']['number'];
+    $card_expire_month = $pane_values['credit_card']['exp_month'];
+    $card_expire_year = $pane_values['credit_card']['exp_year'];
+    $card_expire = $card_expire_year . '-' . $card_expire_month;
+    $card_code = $pane_values['credit_card']['code'];
+    $card_type = $pane_values['credit_card']['type'];
+
+    $card_first_name = $billing_address['first_name'];
+    $card_last_name = $billing_address['last_name'];
+
+    $card = commerce_cardonfile_new();
+    $card->uid = $order->uid;
+    $card->payment_method = $payment_method['method_id'];
+    $card->instance_id = $payment_method['instance_id'];
+    $card->card_type = !empty($card_type) ? $card_type : 'card';
+    $card->card_exp_month = $card_expire_month;
+    $card->card_exp_year = $card_expire_year;
+    $card->card_name = $card_first_name . ' ' . $card_last_name;
+    $card->card_number = substr($card_number, -4);
+    $card->status = 1;
+
+    // Create the params array for the REST API request.
+    $params = array(
+      'payer_id' => 'user_' . $card->uid,
+      'type' => $card_type,
+      'number' => $card_number,
+      'expire_month' => $card_expire_month,
+      'expire_year' => $card_expire_year,
+      'first_name' => $card_first_name,
+      'last_name' => $card_last_name,
+      'cvv2' => $card_code,
+    );
+    $rest = array(
+      'endpoint' => 'vault/credit-card',
+      'method' => 'POST',
+      'version' => 'v1',
+    );
+
+    $response = commerce_paypal_rest_api_request($payment_method, $rest, $params);
+
+    if (!$response) {
+      // Our request failed. Whoops.
+      return FALSE;
+      drupal_set_message(t('Your credit card was not able to be stored for future use. Please contact the site administrator for additional information.'), 'error');
+    }
+    else {
+      // Op success!
+      $card->remote_id = $response->data->id;
+      commerce_cardonfile_save($card);
+    }
+  }
+}
+
+function commerce_paypal_wpp_vault_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $charge) {
+  // First attempt to load the card on file.
+  $card = commerce_cardonfile_load($pane_values['cardonfile']);
+
+  // Fail now if it is no longer available or the card is inactive.
+  if (empty($card) || $card->status == 0) {
+    drupal_set_message(t('The requested card on file is no longer valid.'), 'error');
+    return FALSE;
+  }
+
+  return commerce_paypal_vault_cardonfile_charge($payment_method, $card, $order, $charge);
 }
 
 /**
