diff --git a/docs/manual/webapi/2.0/resources/diff-context.txt b/docs/manual/webapi/2.0/resources/diff-context.txt
new file mode 100644
index 0000000000000000000000000000000000000000..32203e1763b48e185d95db79066c844c6c3a280c
--- /dev/null
+++ b/docs/manual/webapi/2.0/resources/diff-context.txt
@@ -0,0 +1,5 @@
+.. webapi-resource::
+   :classname: reviewboard.webapi.resources.diff_context.DiffContextResource
+   :hide-examples:
+
+.. comment: vim: ft=rst et ts=3
diff --git a/docs/manual/webapi/2.0/resources/index.txt b/docs/manual/webapi/2.0/resources/index.txt
index fa4b2d366296c379aaa2347f9a191435d43fa6e3..893cacacdad02d0e82149c94c460e8dce51969f6 100644
--- a/docs/manual/webapi/2.0/resources/index.txt
+++ b/docs/manual/webapi/2.0/resources/index.txt
@@ -7,6 +7,7 @@ Resources
 
    change-list
    change
+   diff-context
    diff-list
    diff
    default-reviewer-list
diff --git a/reviewboard/static/rb/js/pages/views/diffViewerPageView.js b/reviewboard/static/rb/js/pages/views/diffViewerPageView.js
index 90fbe5d36999da42c1ab3874f7cbbd74f8289d27..580c965cd7b582370b08a6e69d9ac99c6677ba43 100644
--- a/reviewboard/static/rb/js/pages/views/diffViewerPageView.js
+++ b/reviewboard/static/rb/js/pages/views/diffViewerPageView.js
@@ -305,6 +305,28 @@ RB.DiffViewerPageView = RB.ReviewablePageView.extend({
         /* Check to see if there's an anchor we need to scroll to. */
         url = document.location.toString();
         this._startAtAnchorName = (url.match('#') ? url.split('#')[1] : null);
+
+        this.router = new Backbone.Router({
+            routes: {
+                ':revision/': 'revision'
+            }
+        });
+        this.listenTo(this.router, 'route:revision', function(revision) {
+            var parts;
+
+            if (revision.indexOf('-') === -1) {
+                this._loadRevision(0, parseInt(revision, 10));
+            } else {
+                parts = revision.split('-', 2);
+                this._loadRevision(parseInt(parts[0], 10),
+                                   parseInt(parts[1], 10));
+            }
+        });
+        Backbone.history.start({
+            pushState: true,
+            root: this.options.reviewRequestData.reviewURL + 'diff/',
+            silent: true
+        });
     },
 
     /*
@@ -699,24 +721,49 @@ RB.DiffViewerPageView = RB.ReviewablePageView.extend({
     },
 
     /*
-     * Callback when a revision is selected.
+     * Callback for when a new revision is selected.
      *
-     * Navigates to the selected revision of the diff. If `base` is 0, this
-     * will show the single diff revision given in `tip`. Otherwise, this will
-     * show an interdiff between `base` and `tip`.
-     *
-     * TODO: this should show the new revision without reloading the page.
+     * This supports both single revisions and interdiffs. If `base` is 0, a
+     * single revision is selected. If not, the interdiff between `base` and
+     * `tip` will be shown.
      */
     _onRevisionSelected: function(base, tip) {
-        var url = this.reviewRequest.get('reviewURL');
+        if (base === 0) {
+            this.router.navigate(tip + '/', {trigger: true});
+        } else {
+            this.router.navigate(base + '-' + tip + '/', {trigger: true});
+        }
+    },
+
+    /*
+     * Load a given revision.
+     *
+     * This supports both single revisions and interdiffs. If `base` is 0, a
+     * single revision is selected. If not, the interdiff between `base` and
+     * `tip` will be shown.
+     */
+    _loadRevision: function(base, tip) {
+        var reviewRequestURL = _.result(this.reviewRequest, 'url'),
+            contextURL = reviewRequestURL + 'diff-context/';
 
         if (base === 0) {
-            url += 'diff/' + tip + '/#index_header';
+            contextURL += '?revision=' + tip;
         } else {
-            url += 'diff/' + base + '-' + tip + '/#index_header';
+            contextURL += '?revision=' + base + '&interdiff_revision=' + tip;
         }
 
-        window.location = url;
+        $.ajax(contextURL).done(_.bind(function(rsp) {
+            var context = rsp.diff_context,
+                files,
+                url;
+
+            _.each(this._diffReviewableViews, function(diffReviewableView) {
+                diffReviewableView.remove();
+            });
+            this._diffReviewableViews = [];
+
+            this.model.set(this.model.parse(context));
+        }, this));
     }
 });
 _.extend(RB.DiffViewerPageView.prototype, RB.KeyBindingsMixin);
diff --git a/reviewboard/webapi/resources/diff_context.py b/reviewboard/webapi/resources/diff_context.py
new file mode 100644
index 0000000000000000000000000000000000000000..e331e6d387215f970e2cacf8c50c7c0b51e1a967
--- /dev/null
+++ b/reviewboard/webapi/resources/diff_context.py
@@ -0,0 +1,69 @@
+from django.http import Http404
+from djblets.webapi.decorators import (webapi_response_errors,
+                                       webapi_request_fields)
+from djblets.webapi.errors import DOES_NOT_EXIST
+
+from reviewboard.reviews.views import ReviewsDiffViewerView
+from reviewboard.webapi.base import WebAPIResource
+from reviewboard.webapi.decorators import webapi_check_local_site
+
+
+class DiffViewerContextView(ReviewsDiffViewerView):
+    # We piggy-back on the ReviewsDiffViewerView to do all the heavy
+    # lifting. By overriding render_to_response, we don't have to render it
+    # to HTML, and can just return the data that we need from javascript.
+    def render_to_response(self, context, **kwargs):
+        return context
+
+
+class DiffContextResource(WebAPIResource):
+    """Provides context information for a specific diff view.
+
+    The output of this is more or less internal to the Review Board web UI.
+    This will return the various pieces of information required to render a
+    diff view for a given diff revision/interdiff. This is used to re-render
+    the diff viewer without a reload when navigating between revisions.
+    """
+    # The javascript side of this is in DiffViewerPageModel and it's associated
+    # sub-models.
+    name = 'diff_context'
+    singleton = True
+
+    @webapi_check_local_site
+    @webapi_request_fields(
+        optional={
+            'revision': {
+                'type': int,
+                'description': 'Which revision of the diff to show.',
+            },
+            'interdiff_revision': {
+                'type': int,
+                'description': 'A tip revision for showing interdiffs. If '
+                               'this is provided, the ``revision`` field will '
+                               'be the base diff.',
+            },
+        },
+    )
+    @webapi_response_errors(DOES_NOT_EXIST)
+    def get(self, request, review_request_id, revision=None,
+            interdiff_revision=None, local_site_name=None, *args, **kwargs):
+        """Returns the context information for a particular revision or interdiff.
+
+        The output of this is more or less internal to the Review Board web UI.
+        The result will be an object with several fields for the files in the
+        diff, pagination information, and other data which is used to render
+        the diff viewer page.
+        """
+        try:
+            view = DiffViewerContextView.as_view()
+            context = view(request, review_request_id, revision,
+                           interdiff_revision, local_site_name)
+        except Http404:
+            return DOES_NOT_EXIST
+
+        return 200, {
+            self.item_result_key: context['diff_context'],
+        }
+
+
+diff_context_resource = DiffContextResource()
diff --git a/reviewboard/webapi/resources/review_request.py b/reviewboard/webapi/resources/review_request.py
index bb44d3336966c3e82bb81cb8851c4fbe4c4574ab..cac04c86d0afd8b14341ea8c5f023a139cd8bb73 100644
--- a/reviewboard/webapi/resources/review_request.py
+++ b/reviewboard/webapi/resources/review_request.py
@@ -149,6 +149,7 @@ class ReviewRequestResource(WebAPIResource):
     item_child_resources = [
         resources.change,
         resources.diff,
+        resources.diff_context,
         resources.review_request_draft,
         resources.review_request_last_update,
         resources.review,
