Using migrate with a multi-page / paginated source I have a working module pulling a JSON feed into Drupal using the Migrate module. However, the feed is paginated, and I am only getting the first page of results (ordered DESC on creation, so giving the latest items). 

I need to go back through all pages and get all/any changed items, but I can't see how to tell Migrate to iterate through the pages.
You can parse paginated sources with migrate but it takes a little work overriding MigrateListJSON - you may want to ignore the $jsonContainer part - that's just because the result set in our service is in a container (hence this class is abstract). 

abstract class BSBaseMigrateListJSON extends MigrateListJSON {
  /**
   * String containing the JSON container name.
   */
  protected $jsonContainer;

  /**
   * Contains the default limit for pagination of the service.
   */
  protected $serviceLimit = 50;

  /**
   * Return an array of the IDs found in the list URL.
   */
  public function getIdList() {
    migrate_instrument_start("Retrieve $this-&gt;listUrl");
    $data = $this-&gt;getTree();
    migrate_instrument_stop("Retrieve $this-&gt;listUrl");
    if ($data) {
      return $this-&gt;getIDsFromJSON($data);
    }
    Migration::displayMessage(t('Loading of !listurl failed:', array('!listurl' =&gt; $this-&gt;listUrl)));
    return NULL;
  }

  /**
   * Returns the amount of items to be processed.
   */
  public function computeCount() {
    if ($data = $this-&gt;getTree()) {
      return count($data[$this-&gt;jsonContainer]);
    }
    // Else fail and return N/A results.
    return NULL;
  }

  /**
   * Returns an array of crids.
   */
  protected function getIDsFromJSON(array $data) {
    $ids = array();
    foreach ($data[$this-&gt;jsonContainer] as $item) {
      $ids[] = $item['id'];
    }
    return $ids;
  }

  /**
   * Returns the list URL decoded JSON response.
   */
  protected function getTree() {
    $items = array();
    $offset = 0;

    do {
      $url = $this-&gt;rebuildUrl($offset);
      $json = $this-&gt;getTreeData($url);
      $data = drupal_json_decode($json);
      $items = array_merge($items, $data[$this-&gt;jsonContainer]);
      $offset += $this-&gt;serviceLimit;
    }
    while ($data['moreResources'] == TRUE);

    // Return data if any is present.
    if (!empty($data)) {
      $data[$this-&gt;jsonContainer] = $items;
      // @todo Consider whether just returning $items is feasible.
      return $data;
    }

    Migration::displayMessage(t('Loading of !listurl failed:', array('!listurl' =&gt; $this-&gt;listUrl)));
    return NULL;
  }

  /**
   * Returns a constructed url with offset if required.
   */
  private function rebuildUrl($offset = 0) {
    $url_parts = parse_url($this-&gt;listUrl);

    // Build the query parameters.
    if (isset($url_parts['query'])) {
      parse_str($url_parts['query'], $url_parts['qparts']);
      foreach ($url_parts['qparts'] as $key =&gt; $value) {
        if ($value === '') {
          $url_parts['qparts'][$key] = NULL;
        }
        elseif ($key === 'q') {
          $url_parts['path'] = $value;
          unset($url_parts['qparts']['q']);
        }
      }
    }

    // If the offset is zero then we can simply return the original list URL.
    if ($offset == 0) {
      // Handles overriding the service limit if there's already a limit set in
      // the list URL.
      if (!empty($url_parts['qparts']['limit'])) {
        $this-&gt;serviceLimit = $url_parts['qparts']['limit'];
      }
      return $this-&gt;listUrl;
    }

    // Add the offset to the query parameters.
    $url_parts['qparts']['offset'] = $offset;

    // Build parameters to pass to the url function.
    $url_params = array(
      'path' =&gt; $url_parts['scheme'] . '://' . $url_parts['host'] . $url_parts['path'],
      'options' =&gt; array(
        'query' =&gt; isset($url_parts['qparts']) ? $url_parts['qparts'] : NULL,
        'fragment' =&gt; isset($url_parts['fragment']) ? $url_parts['fragment'] : NULL,
        'external' =&gt; TRUE,
        'https' =&gt; (bool) $url_parts['scheme'] == 'https',
      ),
    );

    return url($url_params['path'], $url_params['options']);
  }

  /**
   * Returns the data from the given list URL.
   */
  private function getTreeData($list_url) {
    if (empty($this-&gt;httpOptions)) {
      return file_get_contents($list_url);
    }
    $response = drupal_http_request($list_url, $this-&gt;httpOptions);
    if (!empty($response-&gt;data)) {
      return $response-&gt;data;
    }
  }
}


This code probably won't work as-is for you as it's specific for our service, you'll most definitely need to change this line: while ($data['moreResources'] == TRUE); and anything to do with offset depending on how your service works but it's a start for how to proceed.