diff --git a/CRM/Core/BAO/PrevNextCache.php b/CRM/Core/BAO/PrevNextCache.php
index bdc8e44ba131789db47434a891c315add1098a43..7679ff9977efcefd380583afed04d4d8e01a2866 100644
--- a/CRM/Core/BAO/PrevNextCache.php
+++ b/CRM/Core/BAO/PrevNextCache.php
@@ -496,4 +496,26 @@ AND        c.created_date < date_sub( NOW( ), INTERVAL %2 day )
     ];
   }
 
+  /**
+   * Generate and assign an arbitrary value to a field of a test object.
+   *
+   * This specifically supports testing the dedupe use case.
+   *
+   * @param string $fieldName
+   * @param array $fieldDef
+   * @param int $counter
+   *   The globally-unique ID of the test object.
+   */
+  protected function assignTestValue($fieldName, &$fieldDef, $counter) {
+    if ($fieldName === 'cacheKey') {
+      $this->cacheKey = 'merge_' . rand();
+      return;
+    }
+    if ($fieldName === 'data') {
+      $this->data = serialize([]);
+      return;
+    }
+    parent::assignTestValue($fieldName, $fieldDef, $counter);
+  }
+
 }
diff --git a/CRM/Dedupe/Merger.php b/CRM/Dedupe/Merger.php
index 26b854fad6ed262a1e886cf575b59c6e3a24dbd8..ef99ee7719e92b2744a23baa1d9fb08439d4913c 100644
--- a/CRM/Dedupe/Merger.php
+++ b/CRM/Dedupe/Merger.php
@@ -752,13 +752,13 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
     }
 
     // get previous stats
-    $previousStats = CRM_Core_BAO_PrevNextCache::retrieve("{$cacheKeyString}_stats");
+    $previousStats = CRM_Dedupe_Merger::getMergeStats($cacheKeyString);
     if (!empty($previousStats)) {
-      if ($previousStats[0]['merged']) {
-        $merged = $merged + $previousStats[0]['merged'];
+      if ($previousStats['merged']) {
+        $merged = $merged + $previousStats['merged'];
       }
-      if ($previousStats[0]['skipped']) {
-        $skipped = $skipped + $previousStats[0]['skipped'];
+      if ($previousStats['skipped']) {
+        $skipped = $skipped + $previousStats['skipped'];
       }
     }
 
@@ -793,13 +793,15 @@ INNER JOIN  civicrm_membership membership2 ON membership1.membership_type_id = m
    *
    * @return array
    *   Array of how many were merged and how many were skipped.
+   *
+   * @throws \CiviCRM_API3_Exception
    */
   public static function getMergeStats($cacheKeyString) {
-    $stats = CRM_Core_BAO_PrevNextCache::retrieve("{$cacheKeyString}_stats");
+    $stats = civicrm_api3('Dedupe', 'get', ['cachekey' => "{$cacheKeyString}_stats", 'sequential' => 1])['values'];
     if (!empty($stats)) {
-      $stats = $stats[0];
+      return $stats[0]['data'];
     }
-    return $stats;
+    return [];
   }
 
   /**
diff --git a/api/v3/Dedupe.php b/api/v3/Dedupe.php
new file mode 100644
index 0000000000000000000000000000000000000000..e98f1d14fff5bfb15c3d7d64d65d4ff8e71846b9
--- /dev/null
+++ b/api/v3/Dedupe.php
@@ -0,0 +1,150 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 5                                                  |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2019                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * This api exposes CiviCRM dedupe functionality.
+ *
+ * @package CiviCRM_APIv3
+ */
+
+/**
+ * Get rows for any cached attempted merges on the passed criteria.
+ *
+ * @param array $params
+ *
+ * @return array
+ * @throws \API_Exception
+ */
+function civicrm_api3_dedupe_get($params) {
+  $sql = CRM_Utils_SQL_Select::fragment();
+  $sql->where(['merge_data_restriction' => "cachekey LIKE 'merge_%'"]);
+
+  if (isset($params['cachekey'])) {
+    // This is so bad. We actually have a camel case field name in the DB. Don't do that.
+    // Intercept the pain here.
+    $params['cacheKey'] = $params['cachekey'];
+    unset($params['cachekey']);
+  }
+
+  $options = _civicrm_api3_get_options_from_params($params, TRUE, 'PrevNextCache', 'get');
+  $result = _civicrm_api3_basic_get('CRM_Core_BAO_PrevNextCache', $params, FALSE, 'PrevNextCache', $sql);
+
+  if ($options['is_count']) {
+    return civicrm_api3_create_success($result, $params, 'PrevNextCache', 'get');
+  }
+  foreach ($result as $index => $values) {
+    if (isset($values['data']) && !empty($values['data'])) {
+      $result[$index]['data'] = unserialize($values['data']);
+    }
+    if (isset($values['cacheKey'])) {
+      $result[$index]['cachekey'] = $result[$index]['cacheKey'];
+      unset($result[$index]['cacheKey']);
+    }
+  }
+  return civicrm_api3_create_success($result, $params, 'PrevNextCache');
+}
+
+/**
+ * Get rows for getting dedupe cache records.
+ *
+ * @param array $params
+ */
+function _civicrm_api3_dedupe_get_spec(&$params) {
+  $params = CRM_Core_DAO_PrevNextCache::fields();
+  $params['id']['api.aliases'] = ['dedupe_id'];
+}
+
+/**
+ * Delete rows for any cached attempted merges on the passed criteria.
+ *
+ * @param array $params
+ *
+ * @return array
+ *
+ * @throws \API_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+function civicrm_api3_dedupe_delete($params) {
+  return _civicrm_api3_basic_delete('CRM_Core_BAO_PrevNextCache', $params);
+}
+
+/**
+ * Get the statistics for any cached attempted merges on the passed criteria.
+ *
+ * @param array $params
+ *
+ * @return array
+ * @throws \API_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+function civicrm_api3_dedupe_create($params) {
+  return _civicrm_api3_basic_create('CRM_Core_BAO_PrevNextCache', $params, 'PrevNextCache');
+}
+
+/**
+ * Get the statistics for any cached attempted merges on the passed criteria.
+ *
+ * @param array $params
+ *
+ * @return array
+ * @throws \CiviCRM_API3_Exception
+ */
+function civicrm_api3_dedupe_getstatistics($params) {
+  $stats = CRM_Dedupe_Merger::getMergeStats(CRM_Dedupe_Merger::getMergeCacheKeyString(
+    $params['rule_group_id'],
+    CRM_Utils_Array::value('group_id', $params),
+    CRM_Utils_Array::value('criteria', $params, []),
+    CRM_Utils_Array::value('check_permissions', $params, [])
+  ));
+  return civicrm_api3_create_success($stats);
+}
+
+/**
+ * Adjust Metadata for Create action.
+ *
+ * The metadata is used for setting defaults, documentation & validation.
+ *
+ * @param array $params
+ *   Array of parameters determined by getfields.
+ */
+function _civicrm_api3_dedupe_getstatistics_spec(&$params) {
+  $params['rule_group_id'] = [
+    'title' => ts('Rule Group ID'),
+    'api.required' => TRUE,
+    'type' => CRM_Utils_Type::T_INT,
+  ];
+  $params['group_id'] = [
+    'title' => ts('Group ID'),
+    'api.required' => FALSE,
+    'type' => CRM_Utils_Type::T_INT,
+  ];
+  $params['criteria'] = [
+    'title' => ts('Criteria'),
+    'description' => ts('Dedupe search criteria, as parsable by v3 Contact.get api'),
+  ];
+
+}
diff --git a/api/v3/utils.php b/api/v3/utils.php
index 1a29319f7ad9008359efa775cae97cc5b17b55d1..d04055a794102e8465f1fb1968f4f2e9fd163158 100644
--- a/api/v3/utils.php
+++ b/api/v3/utils.php
@@ -381,6 +381,11 @@ function _civicrm_api3_get_BAO($name) {
     // has enhanced access to other entities.
     $name = 'Contribution';
   }
+  if ($name === 'Dedupe') {
+    // Dedupe is a pseudoentity for PrevNextCache - but accessing dedupe related info
+    // not the other cache info like search results (which could in fact be in Redis or another cache engine)
+    $name = 'PrevNextCache';
+  }
   $dao = _civicrm_api3_get_DAO($name);
   if (!$dao) {
     return NULL;
diff --git a/tests/phpunit/CRM/Dedupe/MergerTest.php b/tests/phpunit/CRM/Dedupe/MergerTest.php
index 78dffbff2925d6c728c07f9dae144c1bf315921b..c55f1883247d8a3e49c9270b085624401d9a2de3 100644
--- a/tests/phpunit/CRM/Dedupe/MergerTest.php
+++ b/tests/phpunit/CRM/Dedupe/MergerTest.php
@@ -177,7 +177,6 @@ class CRM_Dedupe_MergerTest extends CiviUnitTestCase {
     $select = ['pn.is_selected' => 'is_selected'];
     $cacheKeyString = CRM_Dedupe_Merger::getMergeCacheKeyString($dao->id, $this->_groupId);
     $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
-
     $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
 
     // mark first two pairs as selected
@@ -191,6 +190,13 @@ class CRM_Dedupe_MergerTest extends CiviUnitTestCase {
     $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', 5, 1);
     $this->assertEquals(count($result['merged']), 2, 'Check number of merged pairs.');
 
+    $stats = $this->callAPISuccess('Dedupe', 'getstatistics', [
+      'group_id' => $this->_groupId,
+      'rule_group_id' => $dao->id,
+      'check_permissions' => TRUE,
+    ])['values'];
+    $this->assertEquals(['merged' => 2, 'skipped' => 0], $stats);
+
     // retrieve pairs from prev next cache table
     $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
     $this->assertEquals(count($pnDupePairs), 1, 'Check number of remaining dupe pairs in prev next cache.');
@@ -248,6 +254,10 @@ class CRM_Dedupe_MergerTest extends CiviUnitTestCase {
     $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', 5, 2);
     $this->assertEquals(count($result['merged']), 3, 'Check number of merged pairs.');
 
+    $stats = $this->callAPISuccess('Dedupe', 'getstatistics', [
+      'rule_group_id' => $dao->id,
+      'group_id' => $this->_groupId,
+    ]);
     // retrieve pairs from prev next cache table
     $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
     $this->assertEquals(count($pnDupePairs), 0, 'Check number of remaining dupe pairs in prev next cache.');
diff --git a/tests/phpunit/api/v3/ContactTest.php b/tests/phpunit/api/v3/ContactTest.php
index 4cab52bddcc779107d2b3b282f5ac8c6678b83dd..b96de41f85ce56cfd4a3d5f28f6fb40005f29696 100644
--- a/tests/phpunit/api/v3/ContactTest.php
+++ b/tests/phpunit/api/v3/ContactTest.php
@@ -97,6 +97,7 @@ class api_v3_ContactTest extends CiviUnitTestCase {
       'civicrm_group_contact',
       'civicrm_saved_search',
       'civicrm_group_contact_cache',
+      'civicrm_prevnext_cache',
     );
 
     $this->quickCleanup($tablesToTruncate, TRUE);
diff --git a/tests/phpunit/api/v3/SyntaxConformanceTest.php b/tests/phpunit/api/v3/SyntaxConformanceTest.php
index e22e8eea0c77e7157314bcd9c809e5329dcfc20e..da5ecebd53706c6a6b9dbc12d41198df136dab9f 100644
--- a/tests/phpunit/api/v3/SyntaxConformanceTest.php
+++ b/tests/phpunit/api/v3/SyntaxConformanceTest.php
@@ -887,6 +887,7 @@ class api_v3_SyntaxConformanceTest extends CiviUnitTestCase {
       'SmsProvider' => 'Provider',
       'AclRole' => 'EntityRole',
       'MailingEventQueue' => 'Queue',
+      'Dedupe' => 'PrevNextCache',
     ];
 
     $usableName = !empty($entitiesWithNamingIssues[$entityName]) ? $entitiesWithNamingIssues[$entityName] : $entityName;