diff --git a/reviewboard/static/rb/js/accountPrefsPage/views/oauthApplicationsView.ts b/reviewboard/static/rb/js/accountPrefsPage/views/oauthApplicationsView.ts
index b2e4ebc848a597abba70450afab2b7cc735d415a..6969956dd41dc1bbedfe963c2d6955be6dbb1993 100644
--- a/reviewboard/static/rb/js/accountPrefsPage/views/oauthApplicationsView.ts
+++ b/reviewboard/static/rb/js/accountPrefsPage/views/oauthApplicationsView.ts
@@ -17,6 +17,8 @@ import {
 } from 'djblets/configForms';
 import { type ListItemAttrs } from 'djblets/configForms/models/listItemModel';
 
+import { API } from 'reviewboard/common';
+
 
 /**
  * Attributes for the OAuthAppItem model
@@ -171,7 +173,7 @@ class OAuthAppItemView extends ConfigFormsListItemView<OAuthAppItem> {
      * Delete the OAuth2 application.
      */
     _onDeleteClicked() {
-        RB.apiCall({
+        API.call({
             method: 'DELETE',
             success: () => this.model.trigger('destroy'),
             url: this.model.get('apiURL'),
diff --git a/reviewboard/static/rb/js/accountPrefsPage/views/oauthTokensView.ts b/reviewboard/static/rb/js/accountPrefsPage/views/oauthTokensView.ts
index 7c8b3701a8d3fdc455be5a8939f5adbe98a32a2f..7a2b549ad88d93c5d0b8415833d73fd4beca8c73 100644
--- a/reviewboard/static/rb/js/accountPrefsPage/views/oauthTokensView.ts
+++ b/reviewboard/static/rb/js/accountPrefsPage/views/oauthTokensView.ts
@@ -17,6 +17,8 @@ import {
 } from 'djblets/configForms';
 import { type ListItemAttrs } from 'djblets/configForms/models/listItemModel';
 
+import { API } from 'reviewboard/common';
+
 
 /**
  * Attributes for tho OAuthTokenItem model.
diff --git a/reviewboard/static/rb/js/common/collections/baseCollection.ts b/reviewboard/static/rb/js/common/collections/baseCollection.ts
index 333b3ba2ab7246f3ac0b03092600a58f4a8e4968..bc5238be80eb80a23a992545bc6c157fe101a01a 100644
--- a/reviewboard/static/rb/js/common/collections/baseCollection.ts
+++ b/reviewboard/static/rb/js/common/collections/baseCollection.ts
@@ -3,6 +3,11 @@
  * */
 import { BaseCollection as SpinaCollection, spina } from '@beanbag/spina';
 
+import {
+    API,
+    BackboneError,
+} from '../utils/apiUtils';
+
 
 /**
  * The base class used for Review Board collections.
@@ -53,7 +58,7 @@ export class BaseCollection<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.fetch(newOptions));
         }
 
@@ -92,7 +97,7 @@ export class BaseCollection<
     ) {
         return Backbone.sync.call(this, method, model, _.defaults({
             error: (xhr, textStatus, errorThrown) => {
-                RB.storeAPIError(xhr);
+                API.storeError(xhr);
 
                 if (_.isFunction(options.error)) {
                     options.error(xhr, textStatus, errorThrown);
diff --git a/reviewboard/static/rb/js/common/resources/collections/repositoryCommitsCollection.ts b/reviewboard/static/rb/js/common/resources/collections/repositoryCommitsCollection.ts
index 86e6b234422e6dcc7ece675288fff02600c9dfbd..866a7baac5d8694135a168c7b8486da91858d710 100644
--- a/reviewboard/static/rb/js/common/resources/collections/repositoryCommitsCollection.ts
+++ b/reviewboard/static/rb/js/common/resources/collections/repositoryCommitsCollection.ts
@@ -7,6 +7,7 @@ import {
     spina,
 } from '@beanbag/spina';
 
+import { API } from '../../utils/apiUtils';
 import { BaseCollection } from '../../collections/baseCollection';
 import {
     type RepositoryCommitAttrs,
@@ -200,7 +201,7 @@ export class RepositoryCommits extends BaseCollection<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.fetchNext(newOptions));
         }
 
diff --git a/reviewboard/static/rb/js/common/resources/collections/resourceCollection.ts b/reviewboard/static/rb/js/common/resources/collections/resourceCollection.ts
index ad7bc568ba1ed73ee19c6ae8b9ad90e3ab9fdca4..308060f8b865cfc1e2e120991d0ebe044bfe053f 100644
--- a/reviewboard/static/rb/js/common/resources/collections/resourceCollection.ts
+++ b/reviewboard/static/rb/js/common/resources/collections/resourceCollection.ts
@@ -5,6 +5,7 @@
 import { spina } from '@beanbag/spina';
 
 import { BaseCollection } from '../../collections/baseCollection';
+import { API } from '../../utils/apiUtils';
 import {
     type ResourceLink,
     BaseResource,
@@ -244,7 +245,7 @@ export class ResourceCollection<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.fetch(newOptions));
         }
 
@@ -338,7 +339,7 @@ export class ResourceCollection<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.fetchPrev(newOptions));
         }
 
@@ -390,7 +391,7 @@ export class ResourceCollection<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.fetchNext(newOptions));
         }
 
@@ -444,7 +445,7 @@ export class ResourceCollection<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.fetchAll(newOptions));
         }
 
diff --git a/reviewboard/static/rb/js/common/resources/models/baseResourceModel.ts b/reviewboard/static/rb/js/common/resources/models/baseResourceModel.ts
index 6fac590e454521be116ea3124ff917e60f5740d9..24e564d0e07cb3e35ed16de7ede270a4304ddf59 100644
--- a/reviewboard/static/rb/js/common/resources/models/baseResourceModel.ts
+++ b/reviewboard/static/rb/js/common/resources/models/baseResourceModel.ts
@@ -10,6 +10,10 @@ import {
 
 import { ExtraData } from '../../models/extraDataModel';
 import { ExtraDataMixin } from '../../models/extraDataMixin';
+import {
+    API,
+    BackboneError,
+} from '../../utils/apiUtils';
 import { type SerializerState } from '../utils/serializers';
 
 
@@ -379,7 +383,7 @@ export class BaseResource<
             console.warn('BaseResource.ready was called using callbacks. ' +
                          'Callers should be updated to use promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, () => this.ready());
         }
 
@@ -436,7 +440,7 @@ export class BaseResource<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, () => this.ensureCreated());
         }
 
@@ -485,7 +489,7 @@ export class BaseResource<
             console.warn('BaseResource.fetch was called using callbacks. ' +
                          'Callers should be updated to use promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.fetch(newOptions));
         }
 
@@ -557,7 +561,7 @@ export class BaseResource<
             console.warn('BaseResource.save was called using callbacks. ' +
                          'Callers should be updated to use promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.save(newOptions));
         }
 
@@ -750,7 +754,7 @@ export class BaseResource<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.destroy(newOptions));
         }
 
@@ -1059,7 +1063,7 @@ export class BaseResource<
         }
 
         syncOptions.error = (xhr, textStatus, jqXHR) => {
-            RB.storeAPIError(xhr);
+            API.storeError(xhr);
 
             const rsp = xhr.errorPayload;
 
diff --git a/reviewboard/static/rb/js/common/resources/models/draftResourceChildModelMixin.ts b/reviewboard/static/rb/js/common/resources/models/draftResourceChildModelMixin.ts
index 119b5f453dbe6ee699bfb869e82e1667d1c67e1a..ffc85c8c7032995f2e1b2721f091aec4f955e356 100644
--- a/reviewboard/static/rb/js/common/resources/models/draftResourceChildModelMixin.ts
+++ b/reviewboard/static/rb/js/common/resources/models/draftResourceChildModelMixin.ts
@@ -1,5 +1,6 @@
 /** Mixin for resources that are children of a draft resource. */
 
+import { API } from '../../utils/apiUtils';
 import { type ReadyOptions } from './baseResourceModel';
 
 
@@ -37,7 +38,7 @@ export const DraftResourceChildModelMixin = {
             console.warn('RB.DraftResourceChildModelMixin.destroy was ' +
                          'called using callbacks. Callers should be updated ' +
                          'to use promises instead.');
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.destroy(newOptions));
         }
 
@@ -77,7 +78,7 @@ export const DraftResourceChildModelMixin = {
             console.warn('RB.DraftResourceChildModelMixin.ready was ' +
                          'called using callbacks. Callers should be updated ' +
                          'to use promises instead.');
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.ready());
         }
 
diff --git a/reviewboard/static/rb/js/common/resources/models/draftResourceModelMixin.ts b/reviewboard/static/rb/js/common/resources/models/draftResourceModelMixin.ts
index 34318be637c3637b047c0debac92971246a59911..024ec3f936143ca85c4431fece31021e131bca2d 100644
--- a/reviewboard/static/rb/js/common/resources/models/draftResourceModelMixin.ts
+++ b/reviewboard/static/rb/js/common/resources/models/draftResourceModelMixin.ts
@@ -2,7 +2,11 @@
  * Mixin for resources that have special "draft" URLs.
  */
 
-import { UserSession } from 'reviewboard/common';
+import { UserSession } from '../../models/userSessionModel';
+import {
+    API,
+    BackboneError,
+} from '../../utils/apiUtils';
 
 
 /**
@@ -64,7 +68,7 @@ export const DraftResourceModelMixin = {
                          'called using callbacks. Callers should be updated ' +
                          'to use promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.ready(newOptions));
         }
 
@@ -117,7 +121,7 @@ export const DraftResourceModelMixin = {
                          'called using callbacks. Callers should be updated ' +
                          'to use promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.destroy(newOptions));
         }
 
diff --git a/reviewboard/static/rb/js/common/resources/models/draftReviewModel.ts b/reviewboard/static/rb/js/common/resources/models/draftReviewModel.ts
index 9b44b20502b15e9c7773eb973cc9160511cec2e7..98709fb587b47841312c119cf7eaaf31661717bb 100644
--- a/reviewboard/static/rb/js/common/resources/models/draftReviewModel.ts
+++ b/reviewboard/static/rb/js/common/resources/models/draftReviewModel.ts
@@ -7,6 +7,7 @@ import {
     spina,
 } from '@beanbag/spina';
 
+import { API } from '../../utils/apiUtils';
 import * as JSONSerializers from '../utils/serializers';
 import {
     type SerializerMap,
@@ -115,7 +116,7 @@ export class DraftReview extends Review<
             console.warn(`RB.DraftReview.publish was called using callbacks.
                           Callers should be updated to use promises instead.`);
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.publish(newOptions));
         }
 
diff --git a/reviewboard/static/rb/js/common/resources/models/draftReviewRequestModel.ts b/reviewboard/static/rb/js/common/resources/models/draftReviewRequestModel.ts
index 7f26a4371bccb17dfb47b5a73675cd10c0c03040..ad98293832cea3d4575cdb5eaa4fe5ea8d08e771 100644
--- a/reviewboard/static/rb/js/common/resources/models/draftReviewRequestModel.ts
+++ b/reviewboard/static/rb/js/common/resources/models/draftReviewRequestModel.ts
@@ -7,6 +7,10 @@ import {
     spina,
 } from '@beanbag/spina';
 
+import {
+    API,
+    BackboneError,
+} from '../../utils/apiUtils';
 import {
     type BaseResourceAttrs,
     type BaseResourceResourceData,
@@ -253,7 +257,7 @@ export class DraftReviewRequest extends BaseResource<
                 'RB.DraftReview.publish was called using callbacks. ' +
                 'Callers should be updated to use promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.publish(newOptions));
         }
 
diff --git a/reviewboard/static/rb/js/common/resources/models/reviewGroupModel.ts b/reviewboard/static/rb/js/common/resources/models/reviewGroupModel.ts
index b443648808d24398f0db6bfb97af7ee9a5d042d5..c7435c44f0de891bbb6e4879c3d26cac75c3f4d3 100644
--- a/reviewboard/static/rb/js/common/resources/models/reviewGroupModel.ts
+++ b/reviewboard/static/rb/js/common/resources/models/reviewGroupModel.ts
@@ -7,6 +7,10 @@ import {
     spina,
 } from '@beanbag/spina';
 
+import {
+    API,
+    BackboneError,
+} from '../../utils/apiUtils';
 import {
     type BaseResourceAttrs,
     type BaseResourceResourceData,
@@ -207,7 +211,7 @@ export class ReviewGroup extends BaseResource<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.setStarred(starred));
         }
 
@@ -253,7 +257,7 @@ export class ReviewGroup extends BaseResource<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.addUser(username));
         }
 
@@ -309,7 +313,7 @@ export class ReviewGroup extends BaseResource<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.removeUser(username));
         }
 
diff --git a/reviewboard/static/rb/js/common/resources/models/reviewReplyModel.ts b/reviewboard/static/rb/js/common/resources/models/reviewReplyModel.ts
index cb48ccc94f41b2c52b7a0473ceea7b233258b991..165efd8734df2babe5a10fed366e728bec713549 100644
--- a/reviewboard/static/rb/js/common/resources/models/reviewReplyModel.ts
+++ b/reviewboard/static/rb/js/common/resources/models/reviewReplyModel.ts
@@ -7,6 +7,10 @@ import {
     spina,
 } from '@beanbag/spina';
 
+import {
+    BackboneError,
+    API,
+} from '../../utils/apiUtils';
 import * as JSONSerializers from '../utils/serializers';
 import {
     type BaseResourceAttrs,
@@ -230,14 +234,14 @@ export class ReviewReply extends BaseResource<
         options: ReviewReplyPublishOptions = {},
         context: unknown = undefined,
     ): Promise<void> {
-        if (_.isFunction(options.success) ||
-            _.isFunction(options.error) ||
-            _.isFunction(options.complete)) {
+        if (typeof options.success === 'function' ||
+            typeof options.error === 'function' ||
+            typeof options.complete === 'function') {
             console.warn('RB.ReviewReply.publish was called using ' +
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(options, context, newOptions =>
+            return API.promiseToCallbacks<void>(options, context, newOptions =>
                 this.publish(newOptions));
         }
 
@@ -295,7 +299,7 @@ export class ReviewReply extends BaseResource<
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
 
-            return RB.promiseToCallbacks(options, context, newOptions =>
+            return API.promiseToCallbacks(options, context, newOptions =>
                 this.discardIfEmpty(newOptions));
         }
 
@@ -334,9 +338,9 @@ export class ReviewReply extends BaseResource<
             const linkName = ReviewReply.COMMENT_LINK_NAMES[linkNameIndex];
             const url = this.get('links')[linkName].href;
 
-            RB.apiCall({
-                error: (model, xhr, options) => reject(
-                    new BackboneError(model, xhr, options)),
+            API.call({
+                error: (xhr) => reject(
+                    new BackboneError(this, xhr, {})),
                 success: rsp => {
                     if (rsp[linkName].length > 0) {
                         resolve(false);
diff --git a/reviewboard/static/rb/js/common/resources/models/reviewRequestModel.ts b/reviewboard/static/rb/js/common/resources/models/reviewRequestModel.ts
index 76e357d11b9ca047350baa42d88d83d6b76843c8..06ddd8e32eacf89d22c01d7c60b4607228d86417 100644
--- a/reviewboard/static/rb/js/common/resources/models/reviewRequestModel.ts
+++ b/reviewboard/static/rb/js/common/resources/models/reviewRequestModel.ts
@@ -9,6 +9,7 @@ import {
 } from '@beanbag/spina';
 
 import { UserSession } from '../../models/userSessionModel';
+import { API } from '../../utils/apiUtils';
 import {
     type BaseResourceAttrs,
     type BaseResourceResourceData,
@@ -645,7 +646,7 @@ export class ReviewRequest extends BaseResource<
      * server, and if there is, trigger the 'updated' event.
      */
     _checkForUpdates() {
-        RB.apiCall({
+        API.call({
             noActivityIndicator: true,
             prefix: this.get('sitePrefix'),
             success: rsp => {
diff --git a/reviewboard/static/rb/js/common/resources/models/tests/baseResourceModelTests.ts b/reviewboard/static/rb/js/common/resources/models/tests/baseResourceModelTests.ts
index 4ab3b408a817d3b4a0b62cbc8de5e8d3f6ab1c66..bc5f698791b793951b0fdeb9b506034cb3cb233d 100644
--- a/reviewboard/static/rb/js/common/resources/models/tests/baseResourceModelTests.ts
+++ b/reviewboard/static/rb/js/common/resources/models/tests/baseResourceModelTests.ts
@@ -8,7 +8,11 @@ import {
     spyOn,
 } from 'jasmine-core';
 
-import { BaseResource } from 'reviewboard/common';
+import {
+    API,
+    BackboneError,
+    BaseResource,
+} from 'reviewboard/common';
 
 
 suite('rb/resources/models/BaseResource', function() {
@@ -377,7 +381,7 @@ suite('rb/resources/models/BaseResource', function() {
 
             model.set('parentObject', parentObject);
 
-            spyOn(RB, 'apiCall').and.callThrough();
+            spyOn(API, 'call').and.callThrough();
             spyOn($, 'ajax').and.callFake(request => {
                 expect(request.type).toBe('POST');
 
@@ -388,7 +392,7 @@ suite('rb/resources/models/BaseResource', function() {
 
             expect(Backbone.Model.prototype.save).toHaveBeenCalled();
             expect(parentObject.ensureCreated).toHaveBeenCalled();
-            expect(RB.apiCall).toHaveBeenCalled();
+            expect(API.call).toHaveBeenCalled();
             expect($.ajax).toHaveBeenCalled();
 
             expect(model.trigger).toHaveBeenCalledWith('saved', {});
@@ -396,7 +400,7 @@ suite('rb/resources/models/BaseResource', function() {
 
         it('With isNew=true and no parentObject', async function() {
             spyOn(Backbone.Model.prototype, 'save').and.callThrough();
-            spyOn(RB, 'apiCall').and.callThrough();
+            spyOn(API, 'call').and.callThrough();
             spyOn($, 'ajax');
 
             try {
@@ -405,7 +409,7 @@ suite('rb/resources/models/BaseResource', function() {
             } catch (err) {
                 expect(Backbone.Model.prototype.save)
                     .not.toHaveBeenCalled();
-                expect(RB.apiCall).not.toHaveBeenCalled();
+                expect(API.call).not.toHaveBeenCalled();
                 expect($.ajax).not.toHaveBeenCalled();
 
                 expect(err.message).toBe(
@@ -443,7 +447,7 @@ suite('rb/resources/models/BaseResource', function() {
 
             spyOn(parentObject, 'ready').and.resolveTo();
 
-            spyOn(RB, 'apiCall').and.callThrough();
+            spyOn(API, 'call').and.callThrough();
             spyOn($, 'ajax').and.callFake(request => {
                 expect(request.type).toBe('PUT');
 
@@ -457,7 +461,7 @@ suite('rb/resources/models/BaseResource', function() {
 
             expect(parentObject.ready).toHaveBeenCalled();
             expect(Backbone.Model.prototype.save).toHaveBeenCalled();
-            expect(RB.apiCall).toHaveBeenCalled();
+            expect(API.call).toHaveBeenCalled();
             expect($.ajax).toHaveBeenCalled();
             expect(model.trigger).toHaveBeenCalledWith('saved', {});
         });
@@ -502,7 +506,7 @@ suite('rb/resources/models/BaseResource', function() {
 
             model.set('parentObject', parentObject);
 
-            spyOn(RB, 'apiCall').and.callThrough();
+            spyOn(API, 'call').and.callThrough();
             spyOn($, 'ajax').and.callFake(request => {
                 expect(request.type).toBe('POST');
 
@@ -515,7 +519,7 @@ suite('rb/resources/models/BaseResource', function() {
                 success: () => {
                     expect(Backbone.Model.prototype.save).toHaveBeenCalled();
                     expect(parentObject.ensureCreated).toHaveBeenCalled();
-                    expect(RB.apiCall).toHaveBeenCalled();
+                    expect(API.call).toHaveBeenCalled();
                     expect($.ajax).toHaveBeenCalled();
 
                     expect(model.trigger).toHaveBeenCalledWith('saved', {});
@@ -541,7 +545,7 @@ suite('rb/resources/models/BaseResource', function() {
 
                 spyOn(model, 'ready').and.resolveTo();
 
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
                 spyOn($, 'ajax').and.callFake(request => {
                     expect(request.url).toBe(model.url);
                     expect(request.contentType)
@@ -567,7 +571,7 @@ suite('rb/resources/models/BaseResource', function() {
                 await model.save();
 
                 expect(model.toJSON).toHaveBeenCalled();
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
         });
@@ -585,7 +589,7 @@ suite('rb/resources/models/BaseResource', function() {
                 };
 
                 spyOn(Backbone.Model.prototype, 'save').and.callThrough();
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
             });
 
             it('With file', async function() {
@@ -638,7 +642,7 @@ suite('rb/resources/models/BaseResource', function() {
                 await model.save({ boundary });
 
                 expect(Backbone.Model.prototype.save).toHaveBeenCalled();
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
 
@@ -714,7 +718,7 @@ suite('rb/resources/models/BaseResource', function() {
                 await model.save({ boundary });
 
                 expect(Backbone.Model.prototype.save).toHaveBeenCalled();
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
 
@@ -736,7 +740,7 @@ suite('rb/resources/models/BaseResource', function() {
                 await model.save();
 
                 expect(Backbone.Model.prototype.save).toHaveBeenCalled();
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
         });
@@ -755,18 +759,18 @@ suite('rb/resources/models/BaseResource', function() {
                 });
 
                 spyOn(Backbone, 'sync').and.callThrough();
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
                 spyOn($, 'ajax');
                 spyOn(form, 'ajaxSubmit').and.callFake(
                     request => request.success({}));
 
                 await model.save({ form: form });
 
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect(form.ajaxSubmit).toHaveBeenCalled();
                 expect($.ajax).not.toHaveBeenCalled();
                 expect(Backbone.sync.calls.argsFor(0)[2].data).toBe(null);
-                expect(RB.apiCall.calls.argsFor(0)[0].data).toBe(null);
+                expect(API.call.calls.argsFor(0)[0].data).toBe(null);
             });
 
             it('Overriding file attributes', async function() {
@@ -783,7 +787,7 @@ suite('rb/resources/models/BaseResource', function() {
 
                 spyOn(model, '_saveWithFiles').and.callThrough();
                 spyOn(Backbone, 'sync').and.callThrough();
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
                 spyOn($, 'ajax');
                 spyOn(form, 'ajaxSubmit').and.callFake(
                     request => request.success({}));
@@ -791,11 +795,11 @@ suite('rb/resources/models/BaseResource', function() {
                 await model.save({ form: form });
 
                 expect(model._saveWithFiles).not.toHaveBeenCalled();
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect(form.ajaxSubmit).toHaveBeenCalled();
                 expect($.ajax).not.toHaveBeenCalled();
                 expect(Backbone.sync.calls.argsFor(0)[2].data).toBe(null);
-                expect(RB.apiCall.calls.argsFor(0)[0].data).toBe(null);
+                expect(API.call.calls.argsFor(0)[0].data).toBe(null);
             });
         });
     });
diff --git a/reviewboard/static/rb/js/common/resources/models/tests/draftReviewRequestModelTests.ts b/reviewboard/static/rb/js/common/resources/models/tests/draftReviewRequestModelTests.ts
index eccb6ebed563c08b0b6c707275824ba34d163e16..2b180f1aa4d7983ad50d1cf53a6db405fad09ebc 100644
--- a/reviewboard/static/rb/js/common/resources/models/tests/draftReviewRequestModelTests.ts
+++ b/reviewboard/static/rb/js/common/resources/models/tests/draftReviewRequestModelTests.ts
@@ -9,6 +9,7 @@ import {
 
 import {
     type DraftReviewRequest,
+    API,
     ReviewRequest,
 } from 'reviewboard/common';
 
@@ -39,7 +40,7 @@ suite('rb/resources/models/DraftReviewRequest', function() {
 
     describe('publish', function() {
         it('With promises', async function() {
-            spyOn(RB, 'apiCall').and.callThrough();
+            spyOn(API, 'call').and.callThrough();
             spyOn($, 'ajax').and.callFake(request => {
                 expect(request.data.public).toBe(1);
 
@@ -64,12 +65,12 @@ suite('rb/resources/models/DraftReviewRequest', function() {
 
             await draft.publish();
 
-            expect(RB.apiCall).toHaveBeenCalled();
+            expect(API.call).toHaveBeenCalled();
             expect($.ajax).toHaveBeenCalled();
         });
 
         it('With callbacks', function(done) {
-            spyOn(RB, 'apiCall').and.callThrough();
+            spyOn(API, 'call').and.callThrough();
             spyOn($, 'ajax').and.callFake(request => {
                 expect(request.data.public).toBe(1);
 
@@ -95,7 +96,7 @@ suite('rb/resources/models/DraftReviewRequest', function() {
 
             draft.publish({
                 success: () => {
-                    expect(RB.apiCall).toHaveBeenCalled();
+                    expect(API.call).toHaveBeenCalled();
                     expect($.ajax).toHaveBeenCalled();
                     expect(console.warn).toHaveBeenCalled();
 
diff --git a/reviewboard/static/rb/js/common/resources/models/tests/reviewGroupModelTests.ts b/reviewboard/static/rb/js/common/resources/models/tests/reviewGroupModelTests.ts
index 62e73cc2a022db282e908fb4c2d9f4d74465a4b8..de7346007ade8e1c2e94064ba7dd29bd23ce3a81 100644
--- a/reviewboard/static/rb/js/common/resources/models/tests/reviewGroupModelTests.ts
+++ b/reviewboard/static/rb/js/common/resources/models/tests/reviewGroupModelTests.ts
@@ -8,6 +8,7 @@ import {
 } from 'jasmine-core';
 
 import {
+    API,
     ReviewGroup,
     UserSession,
 } from 'reviewboard/common';
@@ -33,7 +34,7 @@ suite('rb/resources/models/ReviewGroup', function() {
             spyOn(session.watchedGroups, 'addImmediately').and.callThrough();
             spyOn(session.watchedGroups, 'removeImmediately')
                 .and.callThrough();
-            spyOn(RB, 'apiCall').and.callThrough();
+            spyOn(API, 'call').and.callThrough();
         });
 
         it('true', async function() {
@@ -50,7 +51,7 @@ suite('rb/resources/models/ReviewGroup', function() {
 
             expect(session.watchedGroups.addImmediately)
                 .toHaveBeenCalled();
-            expect(RB.apiCall).toHaveBeenCalled();
+            expect(API.call).toHaveBeenCalled();
             expect($.ajax).toHaveBeenCalled();
         });
 
@@ -68,7 +69,7 @@ suite('rb/resources/models/ReviewGroup', function() {
 
             expect(session.watchedGroups.removeImmediately)
                 .toHaveBeenCalled();
-            expect(RB.apiCall).toHaveBeenCalled();
+            expect(API.call).toHaveBeenCalled();
             expect($.ajax).toHaveBeenCalled();
         });
 
@@ -88,7 +89,7 @@ suite('rb/resources/models/ReviewGroup', function() {
                 success: () => {
                     expect(session.watchedGroups.addImmediately)
                         .toHaveBeenCalled();
-                    expect(RB.apiCall).toHaveBeenCalled();
+                    expect(API.call).toHaveBeenCalled();
                     expect($.ajax).toHaveBeenCalled();
                     expect(console.warn).toHaveBeenCalled();
 
@@ -107,7 +108,7 @@ suite('rb/resources/models/ReviewGroup', function() {
                 name: 'test-group',
             });
 
-            spyOn(RB, 'apiCall').and.callThrough();
+            spyOn(API, 'call').and.callThrough();
         });
 
         it('Loaded group', async function() {
@@ -121,7 +122,7 @@ suite('rb/resources/models/ReviewGroup', function() {
             });
 
             await group.addUser('my-user');
-            expect(RB.apiCall).toHaveBeenCalled();
+            expect(API.call).toHaveBeenCalled();
             expect($.ajax).toHaveBeenCalled();
         });
 
@@ -139,7 +140,7 @@ suite('rb/resources/models/ReviewGroup', function() {
             group.addUser('my-user', {
                 error: () => done.fail(),
                 success: () => {
-                    expect(RB.apiCall).toHaveBeenCalled();
+                    expect(API.call).toHaveBeenCalled();
                     expect($.ajax).toHaveBeenCalled();
                     expect(console.warn).toHaveBeenCalled();
 
@@ -157,7 +158,7 @@ suite('rb/resources/models/ReviewGroup', function() {
             await expectAsync(group.addUser('my-user')).toBeRejectedWith(
                 Error('Unable to add to the group.'));
 
-            expect(RB.apiCall).not.toHaveBeenCalled();
+            expect(API.call).not.toHaveBeenCalled();
             expect($.ajax).not.toHaveBeenCalled();
         });
     });
@@ -171,7 +172,7 @@ suite('rb/resources/models/ReviewGroup', function() {
                 name: 'test-group',
             });
 
-            spyOn(RB, 'apiCall').and.callThrough();
+            spyOn(API, 'call').and.callThrough();
         });
 
         it('Loaded group', async function() {
@@ -182,7 +183,7 @@ suite('rb/resources/models/ReviewGroup', function() {
             });
 
             await group.removeUser('my-user');
-            expect(RB.apiCall).toHaveBeenCalled();
+            expect(API.call).toHaveBeenCalled();
             expect($.ajax).toHaveBeenCalled();
         });
 
@@ -197,7 +198,7 @@ suite('rb/resources/models/ReviewGroup', function() {
             group.removeUser('my-user', {
                 error: () => done.fail(),
                 success: () => {
-                    expect(RB.apiCall).toHaveBeenCalled();
+                    expect(API.call).toHaveBeenCalled();
                     expect($.ajax).toHaveBeenCalled();
                     expect(console.warn).toHaveBeenCalled();
 
@@ -215,7 +216,7 @@ suite('rb/resources/models/ReviewGroup', function() {
             await expectAsync(group.removeUser('my-user')).toBeRejectedWith(
                 Error('Unable to remove from the group.'));
 
-            expect(RB.apiCall).not.toHaveBeenCalled();
+            expect(API.call).not.toHaveBeenCalled();
             expect($.ajax).not.toHaveBeenCalled();
         });
     });
diff --git a/reviewboard/static/rb/js/common/resources/models/tests/reviewReplyModelTests.ts b/reviewboard/static/rb/js/common/resources/models/tests/reviewReplyModelTests.ts
index 97f09be4eb2150aa76e4233aeb227728f778b805..abb5cc7da5b220bc5c5d667fb1e94eecc726d6a6 100644
--- a/reviewboard/static/rb/js/common/resources/models/tests/reviewReplyModelTests.ts
+++ b/reviewboard/static/rb/js/common/resources/models/tests/reviewReplyModelTests.ts
@@ -8,6 +8,7 @@ import {
 } from 'jasmine-core';
 
 import {
+    API,
     BaseResource,
     ReviewReply,
 } from 'reviewboard/common';
@@ -125,7 +126,7 @@ suite('rb/resources/models/ReviewReply', function() {
                     loaded: true,
                 });
 
-                spyOn(RB, 'apiCall').and.callFake(options => {
+                spyOn(API, 'call').and.callFake(options => {
                     const links = model.get('links');
                     const data = {};
                     const key = _.find(
diff --git a/reviewboard/static/rb/js/common/resources/models/tests/reviewRequestModelTests.ts b/reviewboard/static/rb/js/common/resources/models/tests/reviewRequestModelTests.ts
index 2a8d0ce88cdf3143213fc518b3d8eb62010ede99..7cb3a7a0395efe633a8ac57bf920da9d7ec5284e 100644
--- a/reviewboard/static/rb/js/common/resources/models/tests/reviewRequestModelTests.ts
+++ b/reviewboard/static/rb/js/common/resources/models/tests/reviewRequestModelTests.ts
@@ -9,6 +9,7 @@ import {
 } from 'jasmine-core';
 
 import {
+    API,
     ReviewRequest,
     UserSession,
 } from 'reviewboard/common';
@@ -105,7 +106,7 @@ suite('rb/resources/models/ReviewRequest', function() {
 
         describe('reopen', function() {
             it('With promises', async function() {
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
                 spyOn($, 'ajax').and.callFake(request => {
                     expect(request.type).toBe('PUT');
                     expect(request.data.status).toBe('pending');
@@ -121,7 +122,7 @@ suite('rb/resources/models/ReviewRequest', function() {
 
                 await reviewRequest.reopen();
 
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
         });
@@ -164,7 +165,7 @@ suite('rb/resources/models/ReviewRequest', function() {
                     .and.callThrough();
                 spyOn(session.watchedReviewRequests, 'removeImmediately')
                     .and.callThrough();
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
             });
 
             it('true', async function() {
@@ -181,7 +182,7 @@ suite('rb/resources/models/ReviewRequest', function() {
 
                 expect(session.watchedReviewRequests.addImmediately)
                     .toHaveBeenCalled();
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
 
@@ -199,14 +200,14 @@ suite('rb/resources/models/ReviewRequest', function() {
 
                 expect(session.watchedReviewRequests.removeImmediately)
                     .toHaveBeenCalled();
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
         });
 
         describe('close', function() {
             it('With type=CLOSE_DISCARDED', async function() {
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
                 spyOn($, 'ajax').and.callFake(request => {
                     expect(request.type).toBe('PUT');
                     expect(request.data.status).toBe('discarded');
@@ -225,12 +226,12 @@ suite('rb/resources/models/ReviewRequest', function() {
                     type: ReviewRequest.CLOSE_DISCARDED,
                 });
 
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
 
             it('With type=CLOSE_SUBMITTED', async function() {
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
                 spyOn($, 'ajax').and.callFake(request => {
                     expect(request.type).toBe('PUT');
                     expect(request.data.status).toBe('submitted');
@@ -249,23 +250,23 @@ suite('rb/resources/models/ReviewRequest', function() {
                     type: ReviewRequest.CLOSE_SUBMITTED,
                 });
 
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
 
             it('With invalid type', async function() {
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
                 spyOn($, 'ajax');
 
                 await expectAsync(reviewRequest.close({type: 'foo'})).
                     toBeRejectedWith(Error('Invalid close type'));
 
-                expect(RB.apiCall).not.toHaveBeenCalled();
+                expect(API.call).not.toHaveBeenCalled();
                 expect($.ajax).not.toHaveBeenCalled();
             });
 
             it('With description', async function() {
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
                 spyOn($, 'ajax').and.callFake(request => {
                     expect(request.type).toBe('PUT');
                     expect(request.data.status).toBe('submitted');
@@ -285,12 +286,12 @@ suite('rb/resources/models/ReviewRequest', function() {
                     type: ReviewRequest.CLOSE_SUBMITTED,
                 });
 
-                expect(RB.apiCall).toHaveBeenCalled();
+                expect(API.call).toHaveBeenCalled();
                 expect($.ajax).toHaveBeenCalled();
             });
 
             it('With callbacks', function(done) {
-                spyOn(RB, 'apiCall').and.callThrough();
+                spyOn(API, 'call').and.callThrough();
                 spyOn($, 'ajax').and.callFake(request => {
                     expect(request.type).toBe('PUT');
                     expect(request.data.status).toBe('discarded');
@@ -309,7 +310,7 @@ suite('rb/resources/models/ReviewRequest', function() {
                 reviewRequest.close({
                     error: () => done.fail(),
                     success: () => {
-                        expect(RB.apiCall).toHaveBeenCalled();
+                        expect(API.call).toHaveBeenCalled();
                         expect($.ajax).toHaveBeenCalled();
                         expect(console.warn).toHaveBeenCalled();
 
diff --git a/reviewboard/static/rb/js/utils/apiUtils.es6.js b/reviewboard/static/rb/js/common/utils/apiUtils.ts
similarity index 63%
rename from reviewboard/static/rb/js/utils/apiUtils.es6.js
rename to reviewboard/static/rb/js/common/utils/apiUtils.ts
index db39ca0ab1d4d9877abab65841488ac6474cd8bb..c31897332da44aeaec4605edc0abe6243b16d0d7 100644
--- a/reviewboard/static/rb/js/utils/apiUtils.es6.js
+++ b/reviewboard/static/rb/js/common/utils/apiUtils.ts
@@ -1,3 +1,25 @@
+/**
+ * Utilities for interfacing with the Review Board API.
+ */
+
+import { UserSession } from '../models/userSessionModel';
+
+
+/**
+ * An extended XHR object that includes API-related error fields.
+ *
+ * Version Added:
+ *     8.0
+ */
+interface ExtendedXHR extends JQueryXHR {
+    /** The payload from the API response. */
+    errorPayload?: { [key: string]: unknown };
+
+    /** The error text from the API response. */
+    errorText?: string;
+}
+
+
 /**
  * jQuery AJAX transport for receiving Blob and ArrayBuffer data.
  *
@@ -10,7 +32,7 @@ $.ajaxTransport('arraybuffer blob', function(options, origOptions, jqXHR) {
         return null;
     }
 
-    let xhr;
+    let xhr: XMLHttpRequest;
 
     return {
         send(headers, completeCB) {
@@ -51,10 +73,8 @@ $.ajaxTransport('arraybuffer blob', function(options, origOptions, jqXHR) {
              * Catch errors with cross-domain requests, like jQuery does.
              */
             try {
-                for (let key in headers) {
-                    if (headers.hasOwnProperty(key)) {
-                        xhr.setRequestHeader(key, headers[key]);
-                    }
+                for (const [key, header] of Object.entries(headers)) {
+                    xhr.setRequestHeader(key, headers[key]);
                 }
             } catch (e) {}
 
@@ -70,40 +90,66 @@ $.ajaxTransport('arraybuffer blob', function(options, origOptions, jqXHR) {
 });
 
 
+/**
+ * Options for the setActivityIndicator method.
+ *
+ * Version Added:
+ *     8.0
+ */
+export interface SetActivityIndicatorOptions {
+    /** The activity indicator element. This is used for unit testing. */
+    _$activityIndicator?: JQuery;
+
+    /**
+     * Whether to hide the indicator immediately instead of on a delay.
+     *
+     * This is used for unit testing.
+     */
+    _activityIndicatorHideImmediately?: boolean;
+
+    /**
+     * True if the indicator should be suppressed, even if ``enabled`` is true.
+     */
+    noActivityIndicator?: boolean;
+
+    /**
+     * The type of HTTP request that the indicator is representing.
+     */
+    type?: string;
+}
+
+
 /**
  * Enable or disable the activity indicator.
  *
+ * Users should prefer importing API and calling API.setActivityIndicator()
+ * instead of RB.setActivityIndicator().
+ *
  * Args:
  *     enabled (boolean):
  *         Whether the activity indicator should be enabled.
  *
  *     options (object):
  *         Additional options.
- *
- * Option Args:
- *     noActivityIndicator (boolean):
- *         True if the indicator should be suppressed, even if ``enabled`` is
- *         true.
- *
- *     type (string):
- *         The type of HTTP request that the indicator is representing. This
- *         will be either ``GET`` (loading) or ``POST`` (saving).
  */
-RB.setActivityIndicator = function(enabled, options) {
+export function setActivityIndicator(
+    enabled: boolean,
+    options: SetActivityIndicatorOptions,
+) {
     const $activityIndicator = options._$activityIndicator ||
                                $('#activity-indicator');
 
     if (enabled) {
-        if (RB.ajaxOptions.enableIndicator && !options.noActivityIndicator) {
+        if (ajaxOptions.enableIndicator && !options.noActivityIndicator) {
             $activityIndicator.children('.indicator-text')
                 .text((options.type && options.type === 'GET')
-                      ? gettext('Loading...') : gettext('Saving...'));
+                      ? _`Loading...` : _`Saving...`);
 
             $activityIndicator
                 .removeClass('error')
                 .show();
         }
-    } else if (RB.ajaxOptions.enableIndicator &&
+    } else if (ajaxOptions.enableIndicator &&
                !options.noActivityIndicator &&
                !$activityIndicator.hasClass('error')) {
         if (options._activityIndicatorHideImmediately) {
@@ -114,7 +160,38 @@ RB.setActivityIndicator = function(enabled, options) {
                 .fadeOut('fast');
         }
     }
-};
+}
+
+
+/**
+ * Options for the API.call method.
+ *
+ * Version Added:
+ *     8.0
+ */
+export interface APICallOptions
+extends JQuery.AjaxSettings, SetActivityIndicatorOptions {
+    /** Any buttons to disable while the API request is in flight. */
+    buttons?: JQuery;
+
+    /** Additional data to send with the request. */
+    data?: JQuery.PlainObject;
+
+    /** A form to submit, if any. */
+    form?: JQuery;
+
+    /**
+     * The relative path from the server name to the Review Board API
+     * endpoint.
+     */
+    path?: string;
+
+    /** The prefix for the API path (after ``SITE_ROOT`, before ``api``). */
+    prefix?: string;
+
+    /** The type of HTTP request to make. Defaults to ``POST``. */
+    type?: string;
+}
 
 
 /**
@@ -123,70 +200,61 @@ RB.setActivityIndicator = function(enabled, options) {
  * This will handle any button disabling/enabling, write to the correct path
  * prefix, do form uploading, and display server errors.
  *
+ * Users should prefer importing API and calling API.call() instead of
+ * RB.apiCall().
+ *
  * Args:
- *     options (object):
+ *     options (APICallOptions):
  *         Options for the API request.
- *
- * Option Args:
- *     buttons (jQuery):
- *         Any buttons to disable while the API request is in flight.
- *
- *     form (jQuery):
- *         A form to submit, if any.
- *
- *     type (string):
- *         The type of HTTP request to make. Defaults to ``POST``.
- *
- *     prefix (string):
- *         The prefix for the API path (after ``SITE_ROOT``, before ``api``).
- *
- *     path (string):
- *         The relative path from the server name to the Review Board API
- *         endpoint.
- *
- *     data (object):
- *         Additional data to send with the request.
- *
- *     success (function):
- *         An optional success callback. If not specified, the default handler
- *         will reload the page.
- *
- *     error (function):
- *         An optional error callback, to be called after the error banner is
- *         displayed.
- *
- *     complete (function):
- *         An optional complete callback, which is called after the success or
- *         error callbacks.
  */
-RB.apiCall = function(options) {
+export function apiCall(
+    options: APICallOptions,
+) {
     const prefix = options.prefix || '';
     const url = options.url || (SITE_ROOT + prefix + 'api' + options.path);
 
     function showErrorPage(xhr, data) {
-        const $iframe = $('<iframe>').width('100%');
+        const $iframe = <JQuery<HTMLIFrameElement>>$('<iframe>')
+            .width('100%');
         const requestData = options.data ? $.param(options.data) : '(none)';
 
-        $('<div class="server-error-box">')
-            .appendTo(document.body)
-            .append('<p><b>' + gettext('Error Code:') + '</b> ' + xhr.status + '</p>')
-            .append('<p><b>' + gettext('Error Text:') + '</b> ' + xhr.statusText + '</p>')
-            .append('<p><b>' + gettext('Request URL:') + '</b> ' + url + '</p>')
-            .append('<p><b>' + gettext('Request Data:') + '</b> ' + requestData + '</p>')
-            .append('<p class="response-data"><b>' + gettext('Response Data:') + '</b></p>')
-            .append(gettext('<p>There may be useful error details below. The following error page may be useful to your system administrator or when <a href="https://www.reviewboard.org/bugs/new/">reporting a bug</a>. To save the page, right-click the error below and choose "Save Page As," if available, or "View Source" and save the result as a <tt>.html</tt> file.</p>'))
-            .append(gettext('<p><b>Warning:</b> Be sure to remove any sensitive material that may exist in the error page before reporting a bug!</p>'))
+        $(dedent`
+            <div class="server-error-box">
+             <p><b>${_`Error Code:`}</b> ${xhr.status}</p>
+             <p><b>${_`Error Text:`}</b> ${xhr.statusText}</p>
+             <p><b>${_`Request URL:`}</b> ${url}</p>
+             <p><b>${_`Request Data:`}</b> ${requestData}</p>
+             <p class="response-data"><b>${_`Response Data:`}</b></p>
+            </div>
+        `)
+            .append(_`
+                <p>
+                 There may be useful error details below. The following error
+                 page may be useful to your system administrator or when
+                 <a href="https://www.reviewboard.org/bugs/new/">reporting a
+                 bug</a>. To save the page, right-click the error below and
+                 choose "Save Page As," if available, or "View Source" and
+                 save the result as a <tt>.html</tt> file.
+                </p>`)
+            .append(_`
+                <p>
+                 <b>Warning:</b>
+                 Be sure to remove any sensitive material that may exist in
+                 the error page before reporting a bug!
+                </p>`)
             .append($iframe)
+            .appendTo(document.body)
             .on('resize', function() {
                 $iframe.height($(this).height() - $iframe.position().top);
             })
             .modalBox({
                 stretchX: true,
                 stretchY: true,
-                title: gettext('Server Error Details')
+                title: _`Server Error Details`,
             });
 
-        const doc = $iframe[0].contentDocument || $iframe[0].contentWindow.document;
+        const doc = ($iframe[0].contentDocument ||
+                     $iframe[0].contentWindow.document);
         doc.open();
         doc.write(data);
         doc.close();
@@ -197,13 +265,12 @@ RB.apiCall = function(options) {
                                    $('#activity-indicator');
 
         if (options.buttons) {
-            options.buttons.attr('disabled', true);
+            options.buttons.prop('disabled', true);
         }
 
-        RB.setActivityIndicator(true, options);
+        setActivityIndicator(true, options);
 
         const defaultOptions = {
-            url: url,
             data: options.data,
             dataType: options.dataType || 'json',
             error: function(xhr, textStatus, errorThrown) {
@@ -246,6 +313,7 @@ RB.apiCall = function(options) {
                 const responseText = xhr.responseText;
 
                 let rsp = null;
+
                 try {
                     rsp = JSON.parse(responseText);
                 } catch (e) {
@@ -281,17 +349,19 @@ RB.apiCall = function(options) {
 
                 $activityIndicator
                     .addClass('error')
-                    .text(gettext('A server error occurred.'))
+                    .text(_`A server error occurred.`)
                     .append(
                         $('<a href="#">')
-                            .text(gettext('Show Details'))
-                            .click(() => showErrorPage(xhr, responseText))
+                            .text(_`Show Details`)
+                            .on('click',
+                                () => showErrorPage(xhr, responseText))
                     )
                     .append(
                         $('<a href="#">')
-                            .text(gettext('Dismiss'))
-                            .click(function() {
+                            .text(_`Dismiss`)
+                            .on('click', () => {
                                 $activityIndicator.fadeOut('fast');
+
                                 return false;
                             })
                     );
@@ -308,6 +378,7 @@ RB.apiCall = function(options) {
                     options.error(xhr, textStatus, errorThrown);
                 }
             },
+            url: url,
         };
 
         const forcedOptions = {
@@ -323,7 +394,7 @@ RB.apiCall = function(options) {
                     options.buttons.attr('disabled', false);
                 }
 
-                RB.setActivityIndicator(false, options);
+                setActivityIndicator(false, options);
 
                 if (_.isFunction(options.complete)) {
                     options.complete(xhr, status);
@@ -367,20 +438,20 @@ RB.apiCall = function(options) {
     options.type = options.type || 'POST';
 
     if (options.type !== 'GET' && options.type !== 'HEAD' &&
-        RB.UserSession.instance.get('readOnly')) {
+        UserSession.instance.get('readOnly')) {
         console.error('%s request not sent. Site is in read-only mode.',
                       options.type);
         return;
     }
 
     // We allow disabling the function queue for the sake of unit tests.
-    if (RB.ajaxOptions.enableQueuing && options.type !== 'GET') {
+    if (ajaxOptions.enableQueuing && options.type !== 'GET') {
         $.funcQueue('rbapicall').add(doCall);
         $.funcQueue('rbapicall').start();
     } else {
         doCall();
     }
-};
+}
 
 /**
  * Parse API error information from a response and stores it.
@@ -393,7 +464,7 @@ RB.apiCall = function(options) {
  *     xhr (jqXHR):
  *         The XMLHttpRequest object.
  */
-RB.storeAPIError = function(xhr) {
+export function storeAPIError(xhr: ExtendedXHR) {
     try {
         const rsp = JSON.parse(xhr.responseText);
         xhr.errorPayload = rsp;
@@ -402,22 +473,35 @@ RB.storeAPIError = function(xhr) {
         xhr.errorPayload = null;
         xhr.errorText = 'HTTP ' + xhr.status + ' ' + xhr.statusText;
     }
-};
+}
 
 
-RB.ajaxOptions = {
+/**
+ * Global options for ajax operations.
+ */
+export const ajaxOptions = {
+    enableIndicator: true,
     enableQueuing: true,
-    enableIndicator: true
 };
 
 
 /*
- * Call RB.apiCall instead of $.ajax.
+ * Call API.call instead of $.ajax.
  *
- * We wrap instead of assign for now so that we can hook in/override
- * RB.apiCall with unit tests.
+ * We wrap instead of assign, and we explicitly use "API.call" instead of
+ * "apiCall" in order to allow unit tests to create spies for the function.
+ *
+ * TODO: for typing purposes, we really ought to be overriding Backbone.sync
+ * instead of Backbone.ajax. The built-in sync implementation assumes that
+ * Backbone.ajax returns an XHR and uses that to trigger the 'request' event on
+ * the model. We don't currently use that event at all, but we don't care about
+ * the other features of the built-in sync method.
+ *
+ * This would also allow us to modify the backbone type definitions to define
+ * sync result as generic so we could override things to use promise results
+ * instead of jqXHR where appropriate.
  */
-Backbone.ajax = options => RB.apiCall(options);
+Backbone.ajax = options => API.call(options);
 
 
 /**
@@ -426,7 +510,20 @@ Backbone.ajax = options => RB.apiCall(options);
  * Version Added:
  *     5.0
  */
-class BackboneError extends Error {
+export class BackboneError extends Error {
+    /**********************
+     * Instance variables *
+     **********************/
+
+    /** The model or collection that the call was made on. */
+    modelOrCollection: unknown;
+
+    /** The XMLHttpRequest wrapper object. */
+    xhr: ExtendedXHR;
+
+    /** Any options that were passed to the call. */
+    options: unknown;
+
     /**
      * Initialize the error.
      *
@@ -440,7 +537,11 @@ class BackboneError extends Error {
      *     options (object):
      *         Any options that were passed to the call.
      */
-    constructor(modelOrCollection, xhr, options) {
+    constructor(
+        modelOrCollection: unknown,
+        xhr: ExtendedXHR,
+        options: unknown,
+    ) {
         super(xhr.errorText);
 
         this.modelOrCollection = modelOrCollection;
@@ -450,6 +551,10 @@ class BackboneError extends Error {
 }
 
 
+/* Store it as a global for backwards compatibility. */
+window.BackboneError = BackboneError;
+
+
 /**
  * Adapt promises to old-style callbacks.
  *
@@ -457,6 +562,9 @@ class BackboneError extends Error {
  * promise to continue to support old-style success/ready/error/complete
  * callbacks.
  *
+ * Users should prefer importing API and calling API.promiseToCallbacks()
+ * instead of RB.promiseToCallbacks().
+ *
  * Version Added:
  *     5.0
  *
@@ -470,34 +578,47 @@ class BackboneError extends Error {
  *     callable (function):
  *         The function to call.
  */
-RB.promiseToCallbacks = function(options, context, callable) {
-    const success = (
-        _.isFunction(options.success) ? options.success
-            : _.isFunction(options.ready) ? options.ready
-                : undefined);
-    const error = (
-        _.isFunction(options.error) ? options.error : undefined);
-    const complete = (
-        _.isFunction(options.complete) ? options.complete : undefined);
-
+export function promiseToCallbacks<T>(
+    options: {
+        complete?: () => void;
+        error?: (modelOrCollection: unknown, xhr: JQueryXHR,
+                 options: unknown) => void;
+        ready?: (result: T) => void;
+        success?: (result: T) => void;
+    },
+    context: unknown,
+    callable: (options: unknown) => Promise<T>,
+) {
     callable(_.omit(options, ['success', 'ready', 'error', 'complete']))
         .then(result => {
-            if (success) {
-                success.call(context, result);
+            if (typeof options.success === 'function') {
+                options.success.call(context, result);
+            } else if (typeof options.ready === 'function') {
+                options.ready.call(context, result);
             }
         })
-        .catch(err => {
-            if (error) {
-                error.call(context, err.modelOrCollection,
-                           err.xhr, err.options);
+        .catch((err: BackboneError) => {
+            if (typeof options.error === 'function') {
+                options.error.call(context, err.modelOrCollection,
+                                   err.xhr, err.options);
             }
         })
         .finally(() => {
-            if (complete) {
-                complete.call(context);
+            if (typeof options.complete === 'function') {
+                options.complete.call(context);
             }
         });
+}
+
+
+/**
+ * Container object for API methods.
+ *
+ * This is used to facilitate unit tests so jasmine can spy on these methods.
+ */
+export const API = {
+    call: apiCall,
+    promiseToCallbacks,
+    setActivityIndicator,
+    storeError: storeAPIError,
 };
-
-
-// vim: set et:sw=4:
diff --git a/reviewboard/static/rb/js/common/utils/index.ts b/reviewboard/static/rb/js/common/utils/index.ts
index bac86f10b91b7ce35dbde05858d72528e6e3c4ff..cbe33635ed16c3c65029d866b8078ebbed60dda5 100644
--- a/reviewboard/static/rb/js/common/utils/index.ts
+++ b/reviewboard/static/rb/js/common/utils/index.ts
@@ -1,2 +1,11 @@
+export {
+    API,
+    BackboneError,
+    ajaxOptions,
+    apiCall,
+    promiseToCallbacks,
+    setActivityIndicator,
+    storeAPIError,
+} from './apiUtils';
 export * as DataUtils from './dataUtils';
 export { PromiseQueue } from './promiseQueue';
diff --git a/reviewboard/static/rb/js/utils/tests/apiUtilsTests.es6.js b/reviewboard/static/rb/js/common/utils/tests/apiUtilsTests.ts
similarity index 94%
rename from reviewboard/static/rb/js/utils/tests/apiUtilsTests.es6.js
rename to reviewboard/static/rb/js/common/utils/tests/apiUtilsTests.ts
index 73e419e366caf9530772d6a79d8c5c47e3c0eb56..69614fde6f731791c01e988a93eede7c211ac327 100644
--- a/reviewboard/static/rb/js/utils/tests/apiUtilsTests.es6.js
+++ b/reviewboard/static/rb/js/common/utils/tests/apiUtilsTests.ts
@@ -4,6 +4,21 @@
  * Version Added:
  *     5.0
  */
+import { suite } from '@beanbag/jasmine-suites';
+import {
+    afterEach,
+    beforeEach,
+    describe,
+    expect,
+    it,
+} from 'jasmine-core';
+
+import {
+    ajaxOptions,
+    API,
+} from 'reviewboard/common';
+
+
 suite('rb/utils/apiUtils', function() {
     const STATUS_TEXTS = {
         200: 'OK',
@@ -12,20 +27,20 @@ suite('rb/utils/apiUtils', function() {
         500: 'Internal Server Error',
     };
 
-    let oldEnableQueuing;
-    let oldEnableIndicator;
+    let oldEnableQueuing: boolean;
+    let oldEnableIndicator: boolean;
 
     beforeEach(function() {
-        oldEnableQueuing = RB.ajaxOptions.enableQueuing;
-        oldEnableIndicator = RB.ajaxOptions.enableIndicator;
+        oldEnableQueuing = ajaxOptions.enableQueuing;
+        oldEnableIndicator = ajaxOptions.enableIndicator;
 
-        RB.ajaxOptions.enableQueuing = true;
-        RB.ajaxOptions.enableIndicator = true;
+        ajaxOptions.enableQueuing = true;
+        ajaxOptions.enableIndicator = true;
     });
 
     afterEach(function() {
-        RB.ajaxOptions.enableQueuing = oldEnableQueuing;
-        RB.ajaxOptions.enableIndicator = oldEnableIndicator;
+        ajaxOptions.enableQueuing = oldEnableQueuing;
+        ajaxOptions.enableIndicator = oldEnableIndicator;
     });
 
 
@@ -106,11 +121,11 @@ suite('rb/utils/apiUtils', function() {
         });
     }
 
-    describe('apiCall', function() {
+    describe('API.call', function() {
         /**
-         * Run a RB.apiCall test.
+         * Run a API.call test.
          *
-         * This sets up a call to :js:func:`RB.apiCall` with a mock AJAX
+         * This sets up a call to :js:func:`API.call` with a mock AJAX
          * implementation and UI. It handles checking all the common state
          * in the UI and the callbacks, matching expectations against provided
          * arguments.
@@ -207,7 +222,7 @@ suite('rb/utils/apiUtils', function() {
                         }
                     }
 
-                    /* Check DOM state from RB.apiCall. */
+                    /* Check DOM state from API.call. */
                     expect($buttons.attr('disabled')).toBeUndefined();
 
                     const activityIndicatorHasError =
@@ -275,7 +290,7 @@ suite('rb/utils/apiUtils', function() {
                         /*
                          * This seems like a tautology, given the setup code,
                          * but we're making sure it's preserved through
-                         * RB.apiCall's handlers.
+                         * API.call's handlers.
                          */
                         expect(httpStatusText).toBe(STATUS_TEXTS[xhr.status]);
                         expect(xhr.statusText).toBe(httpStatusText);
@@ -293,11 +308,11 @@ suite('rb/utils/apiUtils', function() {
                 spyOn(data, 'error').and.callThrough();
             }
 
-            RB.apiCall(data);
+            API.call(data);
         }
 
         /**
-         * Add a suite of RB.apiCall tests for a given HTTP method.
+         * Add a suite of API.call tests for a given HTTP method.
          *
          * Args:
          *     method (string):
diff --git a/reviewboard/static/rb/js/common/utils/tests/index.ts b/reviewboard/static/rb/js/common/utils/tests/index.ts
index d3d2dcb37b9d985998c05f9af3de171c3ed2eaf6..0123626b46e50602766e066583b5e722b244b080 100644
--- a/reviewboard/static/rb/js/common/utils/tests/index.ts
+++ b/reviewboard/static/rb/js/common/utils/tests/index.ts
@@ -1,2 +1,3 @@
+import './apiUtilsTests';
 import './dataUtilsTests';
 import './promiseQueueTests';
diff --git a/reviewboard/static/rb/js/reviews/models/commentEditorModel.ts b/reviewboard/static/rb/js/reviews/models/commentEditorModel.ts
index 9f66bfd2e31b569a06efced52eee8620e308860f..b55835193622e40eb90a3589833b905581366edb 100644
--- a/reviewboard/static/rb/js/reviews/models/commentEditorModel.ts
+++ b/reviewboard/static/rb/js/reviews/models/commentEditorModel.ts
@@ -9,6 +9,7 @@ import {
 import {
     type BaseComment,
     type ReviewRequest,
+    API,
     ExtraDataMixin,
     UserSession,
 } from 'reviewboard/common';
@@ -264,7 +265,8 @@ export class CommentEditor extends BaseModel<CommentEditorAttrs> {
             console.warn('RB.CommentEditor.save was called using ' +
                          'callbacks. Callers should be updated to use ' +
                          'promises instead.');
-            return RB.promiseToCallbacks(
+
+            return API.promiseToCallbacks(
                 options, context, newOptions => this.save(newOptions));
         }
 
diff --git a/reviewboard/static/rb/js/reviews/models/diffReviewableModel.ts b/reviewboard/static/rb/js/reviews/models/diffReviewableModel.ts
index c6310a0019a76e4edd9278426f3de69df2cc04ef..90253a5b60f97d1782371832b3dd313cdbdf9a88 100644
--- a/reviewboard/static/rb/js/reviews/models/diffReviewableModel.ts
+++ b/reviewboard/static/rb/js/reviews/models/diffReviewableModel.ts
@@ -7,6 +7,9 @@ import {
     spina,
 } from '@beanbag/spina';
 
+import { API } from 'reviewboard/common';
+import { APICallOptions } from 'reviewboard/common/utils/apiUtils';
+
 import {
     type AbstractReviewableAttrs,
     AbstractReviewable,
@@ -207,10 +210,10 @@ export class DiffReviewable
      *     A promise which resolves when the operation is complete.
      */
     _fetchFragment(
-        options, // TODO TYPING: convert once RB.apiCall has an interface
+        options: APICallOptions,
     ): Promise<string> {
         return new Promise<string>(resolve => {
-            RB.apiCall(_.defaults(
+            API.call(_.defaults(
                 {
                     complete: xhr => resolve(xhr.responseText),
                     dataType: 'html',
diff --git a/reviewboard/static/rb/js/reviews/models/tests/reviewRequestEditorModelTests.ts b/reviewboard/static/rb/js/reviews/models/tests/reviewRequestEditorModelTests.ts
index 31adc14e0d38f66020acfd2981b580476e2e1022..e30142fe941dfd84df12f3ad5866990ad64753a4 100644
--- a/reviewboard/static/rb/js/reviews/models/tests/reviewRequestEditorModelTests.ts
+++ b/reviewboard/static/rb/js/reviews/models/tests/reviewRequestEditorModelTests.ts
@@ -8,7 +8,10 @@ import {
     spyOn,
 } from 'jasmine-core';
 
-import { ReviewRequest } from 'reviewboard/common';
+import {
+    BackboneError,
+    ReviewRequest,
+} from 'reviewboard/common';
 import { ReviewRequestEditor } from 'reviewboard/reviews';
 
 
diff --git a/reviewboard/static/rb/js/reviews/utils/diffFragmentQueue.ts b/reviewboard/static/rb/js/reviews/utils/diffFragmentQueue.ts
index ce5b0fc56c18bd49c0479ee9c5def8364c0849ee..0f36c16e6760f6b59de1d767aee494b6176b9c0d 100644
--- a/reviewboard/static/rb/js/reviews/utils/diffFragmentQueue.ts
+++ b/reviewboard/static/rb/js/reviews/utils/diffFragmentQueue.ts
@@ -2,7 +2,10 @@
  * Queue for loading diff fragments.
  */
 
-import { DataUtils } from 'reviewboard/common';
+import {
+    API,
+    DataUtils,
+} from 'reviewboard/common';
 
 import {
     type DiffFragmentViewOptions,
@@ -453,11 +456,11 @@ export class DiffFragmentQueue {
             view = new DiffFragmentView(_.defaults({
                 el: $container,
                 loadDiff: async (options: LoadDiffOptions) => {
-                    RB.setActivityIndicator(true, {type: 'GET'});
+                    API.setActivityIndicator(true, {type: 'GET'});
 
                     await this.#loadDiff(commentID, options);
 
-                    RB.setActivityIndicator(false, {});
+                    API.setActivityIndicator(false, {});
                 },
             }, this.#diffFragmentViewOptions));
 
diff --git a/reviewboard/static/rb/js/reviews/views/diffFragmentView.ts b/reviewboard/static/rb/js/reviews/views/diffFragmentView.ts
index e9a7c43005ab7b3c8bedb7ab68ed3b09bcf15c4b..a49b7211041e8880994b3dd66571aa86007b9648 100644
--- a/reviewboard/static/rb/js/reviews/views/diffFragmentView.ts
+++ b/reviewboard/static/rb/js/reviews/views/diffFragmentView.ts
@@ -8,6 +8,7 @@ import {
     spina,
 } from '@beanbag/spina';
 
+import { API } from 'reviewboard/common';
 import { CenteredElementManager } from 'reviewboard/ui';
 
 import { type LoadDiffOptions } from '../utils/diffFragmentQueue';
@@ -254,7 +255,7 @@ export class DiffFragmentView extends BaseView<
             linesOfContext: $btn.data('lines-of-context'),
         });
 
-        RB.setActivityIndicator(false, {});
+        API.setActivityIndicator(false, {});
 
         /* All our HTML has changed, so clean up and re-render everything. */
         if (this.#centeredMgr !== null) {
diff --git a/reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts b/reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts
index d5ff9c133c80d3fa105f9f28a941833877a8ba6f..868a869e9161bc444eb2fc83e3f6b495b2ae58c8 100644
--- a/reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts
+++ b/reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts
@@ -12,6 +12,7 @@ import {
     type FileAttachment,
     type FileAttachmentComment,
     type ReviewRequest,
+    API,
     FileAttachmentStates,
 } from 'reviewboard/common';
 import {
@@ -887,7 +888,7 @@ export class FileAttachmentThumbnailView extends BaseView<
 
         const model = this.model;
 
-        RB.apiCall({
+        API.call({
             data: {
                 'pending_deletion': false,
             },
diff --git a/reviewboard/static/rb/js/reviews/views/tests/fileAttachmentThumbnailViewTests.ts b/reviewboard/static/rb/js/reviews/views/tests/fileAttachmentThumbnailViewTests.ts
index f9cdfb4dfb86266b8f6bf1b96ed4251db6d2585b..dc9fda13c371a097a2dcdd92b19ff77f4cabef65 100644
--- a/reviewboard/static/rb/js/reviews/views/tests/fileAttachmentThumbnailViewTests.ts
+++ b/reviewboard/static/rb/js/reviews/views/tests/fileAttachmentThumbnailViewTests.ts
@@ -8,6 +8,7 @@ import {
 } from 'jasmine-core';
 
 import {
+    API,
     FileAttachment,
     FileAttachmentStates,
     ReviewRequest,
@@ -397,12 +398,12 @@ suite('rb/views/FileAttachmentThumbnailView', function() {
             spyOn(model, 'url').and.callFake(() => {
                 return '/test-file-attachment/';
             });
-            spyOn(RB, 'apiCall').and.callFake(options => options.success());
+            spyOn(API, 'call').and.callFake(options => options.success());
             spyOn(view, '_onUndoDeleteClicked').and.callThrough();
 
             view.$('.file-undo-delete').click();
 
-            expect(RB.apiCall).toHaveBeenCalled();
+            expect(API.call).toHaveBeenCalled();
             expect(model.get('state'))
                 .toBe(FileAttachmentStates.PUBLISHED);
         });
diff --git a/reviewboard/static/rb/js/reviews/views/unifiedBannerView.ts b/reviewboard/static/rb/js/reviews/views/unifiedBannerView.ts
index 84763c11f07465b45c459eadd6e276343cf15253..4ca4a3d77671291cb55c79c9a6f7f3ef2255da40 100644
--- a/reviewboard/static/rb/js/reviews/views/unifiedBannerView.ts
+++ b/reviewboard/static/rb/js/reviews/views/unifiedBannerView.ts
@@ -19,6 +19,7 @@ import {
 import _ from 'underscore';
 
 import {
+    API,
     ClientCommChannel,
     ReviewRequest,
     UserSession,
@@ -737,7 +738,7 @@ export class UnifiedBannerView extends FloatingBannerView<
         archive: boolean,
     ): Promise<void> {
         return new Promise((resolve, reject) => {
-            RB.apiCall({
+            API.call({
                 data: {
                     batch: JSON.stringify({
                         archive: archive,
diff --git a/reviewboard/static/rb/js/widgets/views/oauthClientSecretView.ts b/reviewboard/static/rb/js/widgets/views/oauthClientSecretView.ts
index b7147bf75dc23dde053364c53ba158192ad63b5e..bc6baea0b40b0f779ce708caffaf1f6d898fa2f8 100644
--- a/reviewboard/static/rb/js/widgets/views/oauthClientSecretView.ts
+++ b/reviewboard/static/rb/js/widgets/views/oauthClientSecretView.ts
@@ -7,6 +7,8 @@ import {
     spina,
 } from '@beanbag/spina';
 
+import { API } from 'reviewboard/common';
+
 
 /**
  * Options for the OAuthClientSecretView.
@@ -110,7 +112,7 @@ export class OAuthClientSecretView extends BaseView<
 
         this.#$regen.attr('aria-busy', 'true');
 
-        RB.apiCall({
+        API.call({
             data: {
                 'regenerate_client_secret': 1,
             },
diff --git a/reviewboard/staticbundles.py b/reviewboard/staticbundles.py
index ef7fff989f3f227cc05b587d202973d1a805d3e8..a1247b32b6487b910156c2602992a0f6c65ae8fb 100644
--- a/reviewboard/staticbundles.py
+++ b/reviewboard/staticbundles.py
@@ -70,7 +70,6 @@ PIPELINE_JAVASCRIPT = {
             'rb/js/ui/views/tests/infoboxManagerViewTests.es6.js',
             'rb/js/ui/views/tests/notificationManagerTests.es6.js',
             'rb/js/ui/views/tests/scrollManagerViewTests.es6.js',
-            'rb/js/utils/tests/apiUtilsTests.es6.js',
             'rb/js/utils/tests/keyBindingUtilsTests.es6.js',
             'rb/js/utils/tests/linkifyUtilsTests.es6.js',
             'rb/js/utils/tests/urlUtilsTests.es6.js',
@@ -88,7 +87,6 @@ PIPELINE_JAVASCRIPT = {
             'rb/js/utils/underscoreUtils.es6.js',
             'rb/js/init.es6.js',
             'rb/js/utils/apiErrors.es6.js',
-            'rb/js/utils/apiUtils.es6.js',
             'rb/js/utils/linkifyUtils.es6.js',
             'rb/js/utils/mathUtils.es6.js',
             'rb/js/utils/keyBindingUtils.es6.js',
