diff --git a/src/DataFetcherPluginBase.php b/src/DataFetcherPluginBase.php
index 9e413fe10..8f53bf392 100644
--- a/src/DataFetcherPluginBase.php
+++ b/src/DataFetcherPluginBase.php
@@ -29,4 +29,10 @@ public static function create(ContainerInterface $container, array $configuratio
     return new static($configuration, $plugin_id, $plugin_definition);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getNextUrls($url) {
+    return [];
+  }
 }
diff --git a/src/DataFetcherPluginInterface.php b/src/DataFetcherPluginInterface.php
index a64b47923..bb6751cc9 100644
--- a/src/DataFetcherPluginInterface.php
+++ b/src/DataFetcherPluginInterface.php
@@ -46,4 +46,16 @@ public function getResponseContent($url);
    */
   public function getResponse($url);
 
+  /**
+   * Collect next urls from the metadata of a paged response.
+   *
+   * Examples of this include HTTP headers and file naming conventions.
+   *
+   * @param string $url
+   *   URL of the resource to check for pager metadata.
+   *
+   * @return array
+   *   Array of URIs.
+   */
+  public function getNextUrls($url);
 }
diff --git a/src/DataParserPluginBase.php b/src/DataParserPluginBase.php
index a1e4c2dd3..748707eb4 100644
--- a/src/DataParserPluginBase.php
+++ b/src/DataParserPluginBase.php
@@ -161,6 +161,9 @@ protected function nextSource() {
       }
 
       if ($this->openSourceUrl($this->urls[$this->activeUrl])) {
+        if (!empty($this->configuration['pager'])) {
+          $this->addNextUrls($this->activeUrl);
+        }
         // We have a valid source.
         return TRUE;
       }
@@ -169,6 +172,36 @@ protected function nextSource() {
     return FALSE;
   }
 
+  /**
+   * Add next page of source data following the active URL.
+   *
+   * @param int $activeUrl
+   *   The index within the source URL array to insert the next URL resource.
+   *   This is parameterized to enable custom plugins to control the ordering of
+   *   next URLs injected into the source URL backlog.
+   */
+  protected function addNextUrls($activeUrl = 0) {
+    $next_urls = $this->getNextUrls($this->urls[$this->activeUrl]);
+
+    if (!empty($next_urls)) {
+      array_splice($this->urls, $activeUrl + 1, 0, $next_urls);
+      $this->urls = array_unique($this->urls);
+    }
+  }
+
+  /**
+   * Collected the next urls from a paged response.
+   *
+   * @param string $url
+   *   URL of the currently active source.
+   *
+   * @return array
+   *   Array of URLs representing next paged resources.
+   */
+  protected function getNextUrls($url) {
+    return $this->getDataFetcherPlugin()->getNextUrls($url);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/Plugin/migrate_plus/data_fetcher/Http.php b/src/Plugin/migrate_plus/data_fetcher/Http.php
index 002abf04f..e27c7909a 100755
--- a/src/Plugin/migrate_plus/data_fetcher/Http.php
+++ b/src/Plugin/migrate_plus/data_fetcher/Http.php
@@ -113,4 +113,25 @@ public function getResponseContent($url) {
     return $response->getBody();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getNextUrls($url) {
+    $next_urls = [];
+
+    $headers = $this->getResponse($url)->getHeader('Link');
+    if (!empty ($headers)) {
+      $headers = explode(',', $headers[0]);
+      foreach ($headers as $header) {
+        $matches = array();
+        preg_match('/^<(.*)>; rel="next"$/', trim($header), $matches);
+        if (!empty($matches) && !empty($matches[1])) {
+          $next_urls[] = $matches[1];
+        }
+      }
+    }
+
+    return array_merge(parent::getNextUrls($url), $next_urls);
+  }
+
 }
diff --git a/src/Plugin/migrate_plus/data_parser/Json.php b/src/Plugin/migrate_plus/data_parser/Json.php
index df9b3fca8..76fe2b7e5 100755
--- a/src/Plugin/migrate_plus/data_parser/Json.php
+++ b/src/Plugin/migrate_plus/data_parser/Json.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\migrate_plus\Plugin\migrate_plus\data_parser;
 
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Url;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\migrate_plus\DataParserPluginBase;
 
@@ -29,43 +31,64 @@ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterfa
    */
   protected $iterator;
 
+  /**
+   * The currently saved source url (as a string.
+   *
+   * @var string
+   */
+  protected $currentUrl;
+
+  /**
+   * The active url's source data.
+   *
+   * @var array
+   */
+  protected $sourceData;
+
   /**
    * Retrieves the JSON data and returns it as an array.
    *
    * @param string $url
    *   URL of a JSON feed.
+   * @param $item_selector
+   *   Selector within the data content at which useful data is found.
    *
    * @return array
    *   The selected data to be iterated.
    *
    * @throws \GuzzleHttp\Exception\RequestException
    */
-  protected function getSourceData($url) {
-    $response = $this->getDataFetcherPlugin()->getResponseContent($url);
+  protected function getSourceData($url, $item_selector) {
 
-    // Convert objects to associative arrays.
-    $source_data = json_decode($response, TRUE);
-
-    // If json_decode() has returned NULL, it might be that the data isn't
-    // valid utf8 - see http://php.net/manual/en/function.json-decode.php#86997.
-    if (is_null($source_data)) {
-      $utf8response = utf8_encode($response);
-      $source_data = json_decode($utf8response, TRUE);
+    // Use saved source data if url is the same as the last time we made the
+    // request.
+    if ($this->currentUrl != $url || !$this->sourceData) {
+      $response = $this->getDataFetcherPlugin()->getResponseContent($url);
+      // Convert objects to associative arrays.
+      $this->sourceData = json_decode($response, TRUE);
+      // If json_decode() has returned NULL, it might be that the data isn't
+      // valid utf8 - see http://php.net/manual/en/function.json-decode.php#86997.
+      if (is_null($this->sourceData)) {
+        $utf8response = utf8_encode($response);
+        $this->sourceData = json_decode($utf8response, TRUE);
+      }
+      $this->currentUrl = $url;
     }
 
     // Backwards-compatibility for depth selection.
-    if (is_int($this->itemSelector)) {
-      return $this->selectByDepth($source_data);
+    if (is_int($item_selector)) {
+      return $this->selectByDepth($this->sourceData);
     }
 
     // Otherwise, we're using xpath-like selectors.
-    $selectors = explode('/', trim($this->itemSelector, '/'));
+    $selectors = explode('/', trim($item_selector, '/'));
+    $return = $this->sourceData;
     foreach ($selectors as $selector) {
       if (!empty($selector)) {
-        $source_data = $source_data[$selector];
+        $return = $return[$selector];
       }
     }
-    return $source_data;
+    return $return;
   }
 
   /**
@@ -104,7 +127,7 @@ protected function selectByDepth(array $raw_data) {
    */
   protected function openSourceUrl($url) {
     // (Re)open the provided URL.
-    $source_data = $this->getSourceData($url);
+    $source_data = $this->getSourceData($url, $this->itemSelector);
     $this->iterator = new \ArrayIterator($source_data);
     return TRUE;
   }
@@ -130,4 +153,52 @@ protected function fetchNextRow() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNextUrls($url) {
+
+    $next_urls = [];
+
+    if (!empty($this->configuration['pager']['type']) && !empty($this->configuration['pager']['selector'])) {
+      $data = $this->getSourceData($url, $this->configuration['pager']['selector']);
+      if ($this->configuration['pager']['type'] == 'urls' && !empty($data)) {
+        if (is_array($data)) {
+          $next_urls = $data;
+        }
+        else {
+          $next_urls[] = $data;
+        }
+      }
+      elseif ($this->configuration['pager']['type'] == 'cursor') {
+        if ($data && is_scalar($data)) {
+          // Just use 'cursor' as a default parameter key if not provided.
+          $key = !empty($this->configuration['pager']['key']) ? $this->configuration['pager']['key'] : 'cursor';
+          // Parse the url and replace the cursor param value and rebuild the url.
+          $path = UrlHelper::parse($url);
+          $path['query'][$key] = $data;
+          $next_urls[] = Url::fromUri($path['path'], [
+            'query' => $path['query'],
+            'fragment' => $path['fragment'],
+          ])->toString();
+        }
+      }
+      elseif ($this->configuration['pager']['type'] == 'page') {
+        if ($data && is_scalar($data)) {
+          // Just use 'page' as a default parameter key if not provided.
+          $key = !empty($this->configuration['pager']['key']) ? $this->configuration['pager']['key'] : 'page';
+          // Parse the url and replace the cursor param value and rebuild the url.
+          $path = UrlHelper::parse($url);
+          $path['query'][$key] = $data + 1;
+          $next_urls[] = Url::fromUri($path['path'], [
+            'query' => $path['query'],
+            'fragment' => $path['fragment'],
+          ])->toString();
+        }
+      }
+    }
+
+    return array_merge(parent::getNextUrls($url), $next_urls);
+  }
+
 }
diff --git a/tests/src/Kernel/MigrateHttpJsonCursoringTest.php b/tests/src/Kernel/MigrateHttpJsonCursoringTest.php
new file mode 100644
index 000000000..103fd7d99
--- /dev/null
+++ b/tests/src/Kernel/MigrateHttpJsonCursoringTest.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\Tests\migrate_plus\Kernel;
+
+use Drupal\Core\Database\Database;
+use Drupal\migrate\MigrateExecutable;
+use Drupal\Tests\migrate\Kernel\MigrateTestBase;
+
+/**
+ * Tests migration destination table.
+ *
+ * @group migrate
+ */
+class MigrateHttpJsonCursoringTest extends MigrateTestBase {
+
+  const TABLE_NAME = 'migrate_test_destination_table';
+
+  /**
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  public static $modules = ['migrate_plus'];
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->connection = Database::getConnection();
+
+    $this->connection->schema()->createTable(static::TABLE_NAME, [
+      'description' => 'Test table',
+      'fields' => [
+        'data' => [
+          'type' => 'varchar',
+          'length' => '32',
+          'not null' => TRUE,
+        ],
+        'data2' => [
+          'type' => 'varchar',
+          'length' => '32',
+          'not null' => TRUE,
+        ],
+        'data3' => [
+          'type' => 'varchar',
+          'length' => '32',
+          'not null' => TRUE,
+        ],
+      ],
+      'primary key' => ['data'],
+    ]);
+  }
+
+  protected function tearDown() {
+    $this->connection->schema()->dropTable(static::TABLE_NAME);
+    parent::tearDown();
+  }
+
+  protected function getTableDestinationMigration() {
+    // Table destination from json input
+    $definition = [
+      'id' => 'migration_http_json_cursoring_test',
+      'migration_tags' => ['Testing'],
+      'source' => [
+        'plugin' => 'url',
+        'data_fetcher_plugin' => 'file',
+        'urls' => 'file:///' . __DIR__ . '/cursoring.json',
+        //'data_fetcher_plugin' => 'http',
+        //'urls' => 'http://www.example.com/modules/contrib/migrate_plus/tests/src/Kernel/cursoring.json',
+        'data_parser_plugin' => 'json',
+        'pager' => [
+          'type' => 'page',
+          'selector' => 'PageNumber',
+        ],
+        'item_selector' => 'List',
+        'fields' => [
+          ['name' => 'data', 'selector' => 'data'],
+          ['name' => 'data2', 'selector' => 'data2'],
+          ['name' => 'data3', 'selector' => 'data3'],
+        ],
+        'ids' => [
+          'data' => ['type' => 'string'],
+        ],
+      ],
+      'destination' => [
+        'plugin' => 'table',
+        'table_name' => static::TABLE_NAME,
+        'id_fields' => ['data' => ['type' => 'string']],
+      ],
+      'process' => [
+        'data' => 'data',
+        'data2' => 'data2',
+        'data3' => 'data3',
+      ],
+    ];
+    return $definition;
+  }
+
+  /**
+   * Tests table destination from json input.
+   */
+  public function testTableDestination() {
+    $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($this->getTableDestinationMigration());
+
+    $executable = new MigrateExecutable($migration, $this);
+    $executable->import();
+
+    $values = $this->connection->select(static::TABLE_NAME)
+      ->fields(static::TABLE_NAME)
+      ->execute()
+      ->fetchAllAssoc('data');
+
+    $this->assertEquals('dummy value', $values['dummy value']->data);
+    $this->assertEquals('dummy2 value', $values['dummy value']->data2);
+    $this->assertEquals('dummy2 value2', $values['dummy value2']->data2);
+    $this->assertEquals('dummy3 value3', $values['dummy value3']->data3);
+    $this->assertEquals(3, count($values));
+  }
+
+}
\ No newline at end of file
diff --git a/tests/src/Kernel/cursoring.json b/tests/src/Kernel/cursoring.json
new file mode 100644
index 000000000..7d7496bbd
--- /dev/null
+++ b/tests/src/Kernel/cursoring.json
@@ -0,0 +1 @@
+{"TotalCount":1234,"PageNumber":1,"PageSize":500,"List":[{"data":"dummy value","data2":"dummy2 value","data3":"dummy3 value"},{"data":"dummy value2","data2":"dummy2 value2","data3":"dummy3 value2"},{"data":"dummy value3","data2":"dummy2 value3","data3":"dummy3 value3"}]}
