diff --git a/opentech/static_src/src/app/src/api/notes.js b/opentech/static_src/src/app/src/api/notes.js
index 7b51bfd2899ab70f80b839bc2b866cbce5a1b0eb..ec431cf28874a8b7baed6976de7a1988a751c5ab 100644
--- a/opentech/static_src/src/app/src/api/notes.js
+++ b/opentech/static_src/src/app/src/api/notes.js
@@ -1,15 +1,20 @@
-import { apiFetch } from '@api/utils';
-
 export function fetchNotesForSubmission(submissionID, visibility = 'internal') {
-    return apiFetch(`/apply/api/submissions/${submissionID}/comments/`, 'GET', {
-        visibility,
-        page_size: 1000,
-    });
+    return {
+        path: `/apply/api/submissions/${submissionID}/comments/`,
+        params: {
+            visibility,
+            page_size: 1000,
+        }
+    };
 }
 
 
 export function createNoteForSubmission(submissionID, note) {
-    return apiFetch(`/apply/api/submissions/${submissionID}/comments/`, 'POST', {}, {
-        body: JSON.stringify(note),
-    });
+    return {
+        path: `/apply/api/submissions/${submissionID}/comments/`,
+        method: 'POST',
+        options: {
+            body: note,
+        }
+    };
 }
diff --git a/opentech/static_src/src/app/src/api/rounds.js b/opentech/static_src/src/app/src/api/rounds.js
index cf8261575443b3539c176b6468ac9886b5f24d98..afd6f8bd22b9de9ae72911ff015fb8f9371931ac 100644
--- a/opentech/static_src/src/app/src/api/rounds.js
+++ b/opentech/static_src/src/app/src/api/rounds.js
@@ -1,5 +1,5 @@
-import { apiFetch } from '@api/utils';
-
 export function fetchRound(id) {
-    return apiFetch(`/apply/api/rounds/${id}/`, 'GET');
+    return {
+        path:`/apply/api/rounds/${id}/`,
+    };
 }
diff --git a/opentech/static_src/src/app/src/api/submissions.js b/opentech/static_src/src/app/src/api/submissions.js
index 8d7f95f776558bdb14a68dc874901214de1dcceb..a1505ebf50ce314aded651753c7abfc4f2285f61 100644
--- a/opentech/static_src/src/app/src/api/submissions.js
+++ b/opentech/static_src/src/app/src/api/submissions.js
@@ -1,13 +1,16 @@
-import { apiFetch } from '@api/utils';
-
-export async function fetchSubmissionsByRound(id) {
-    return apiFetch('/apply/api/submissions/', 'GET', {
-        'round': id,
-        'page_size': 1000,
-    });
+export function fetchSubmissionsByRound(id) {
+    return {
+        path:'/apply/api/submissions/',
+        params: {
+            round: id,
+            page_size: 1000,
+        }
+    };
 }
 
 
-export async function fetchSubmission(id) {
-    return apiFetch(`/apply/api/submissions/${id}/`, 'GET');
+export function fetchSubmission(id) {
+    return {
+        path: `/apply/api/submissions/${id}/`,
+    };
 }
diff --git a/opentech/static_src/src/app/src/api/utils.js b/opentech/static_src/src/app/src/api/utils.js
index 8471e751820ad9bfb9524ba4e0d8762c448c4bb1..ed1c123f417b39a540f2f157cd0bcbdbbe4505d1 100644
--- a/opentech/static_src/src/app/src/api/utils.js
+++ b/opentech/static_src/src/app/src/api/utils.js
@@ -4,7 +4,7 @@ const getBaseUrl = () => {
     return process.env.API_BASE_URL;
 };
 
-export async function apiFetch(path, method = 'GET', params, options = {}) {
+export function apiFetch({path, method = 'GET', params = {}, options = {}}) {
     const url = new URL(getBaseUrl());
     url.pathname = path;
 
diff --git a/opentech/static_src/src/app/src/redux/actions/notes.js b/opentech/static_src/src/app/src/redux/actions/notes.js
index 49b3bacde145424d05dfbedaacac0b5aef7f1380..b0200db84a47cf27a91b4162eaa4fe1bb79c8384 100644
--- a/opentech/static_src/src/app/src/redux/actions/notes.js
+++ b/opentech/static_src/src/app/src/redux/actions/notes.js
@@ -1,4 +1,5 @@
-import { updateSubmission, appendNoteIDForSubmission } from '@actions/submissions';
+import { CALL_API } from '@middleware/api'
+
 import api from '@api';
 
 export const FAIL_FETCHING_NOTES = 'FAIL_FETCHING_NOTES';
@@ -9,86 +10,27 @@ export const UPDATE_NOTE = 'UPDATE_NOTE';
 export const START_CREATING_NOTE_FOR_SUBMISSION = 'START_CREATING_NOTE_FOR_SUBMISSION';
 export const FAIL_CREATING_NOTE_FOR_SUBMISSION = 'FAIL_CREATING_NOTE_FOR_SUBMISSION';
 
-const startFetchingNotes = () => ({
-    type: START_FETCHING_NOTES,
-});
-
-const failFetchingNotes = message => ({
-    type: FAIL_FETCHING_NOTES,
-    message,
-});
-
-export const fetchNotesForSubmission = submissionID => {
-    return async function(dispatch) {
-        dispatch(startFetchingNotes());
-        try {
-            const response = await api.fetchNotesForSubmission(submissionID);
-            const json = await response.json();
-            if (!response.ok) {
-                return dispatch(failFetchingNotes(json.detail));
-            }
-            return dispatch(updateNotesForSubmission(submissionID, json));
-        } catch(e) {
-            return dispatch(failFetchingNotes(e.message));
-        }
-    };
-};
-
-const updateNotesForSubmission = (submissionID, data) => {
-    return function(dispatch) {
-        dispatch(updateNotes(data));
-        dispatch(updateSubmission(submissionID, {
-            comments: data.results.map(v => v.id),
-        }));
-    };
-};
+export const fetchNotesForSubmission = submissionID => (dispatch, getState) => {
+    return dispatch(fetchNotes(submissionID))
+}
 
-export const updateNotes = data => ({
-    type: UPDATE_NOTES,
-    data,
-});
-
-const startCreatingNoteForSubmission = submissionID => ({
-    type: START_CREATING_NOTE_FOR_SUBMISSION,
-    submissionID
-});
-
-const failCreatingNoteForSubmission = (submissionID, error) => ({
-    type: FAIL_CREATING_NOTE_FOR_SUBMISSION,
+const fetchNotes = (submissionID) => ({
+    [CALL_API]: {
+        types: [ START_FETCHING_NOTES, UPDATE_NOTES, FAIL_FETCHING_NOTES],
+        endpoint: api.fetchNotesForSubmission(submissionID),
+    },
     submissionID,
-    error
-});
+})
 
-const updateNote = (data, submissionID) => ({
-    type: UPDATE_NOTE,
-    submissionID,
-    data,
-});
 
-const createdNoteForSubmission = (submissionID, data) => {
-    return function(dispatch) {
-        dispatch(updateNote(data, submissionID));
-        dispatch(appendNoteIDForSubmission(submissionID, data.id));
-        return true;
-    };
-};
+export const createNoteForSubmission = (submissionID, note) => (dispatch, getState) => {
+    return dispatch(createNote(submissionID, note))
+}
 
-export const createNoteForSubmission = (submissionID, note) => {
-    return async function(dispatch) {
-        dispatch(startCreatingNoteForSubmission(submissionID));
-        try {
-            const response = await api.createNoteForSubmission(submissionID, note);
-            const json = await response.json();
-            if (!response.ok) {
-                return dispatch(failCreatingNoteForSubmission(
-                    submissionID,
-                    JSON.stringify(json)
-                ));
-            }
-            return dispatch(createdNoteForSubmission(submissionID, json));
-        } catch (e) {
-            console.error(e);
-            return dispatch(failCreatingNoteForSubmission(submissionID, e.message));
-        }
-    }
-};
+const createNote = (submissionID, note) => ({
+    [CALL_API]: {
+        types: [ START_CREATING_NOTE_FOR_SUBMISSION, UPDATE_NOTE, FAIL_CREATING_NOTE_FOR_SUBMISSION],
+        endpoint: api.createNoteForSubmission(submissionID, note),
+    },
+    submissionID,
+})
diff --git a/opentech/static_src/src/app/src/redux/actions/submissions.js b/opentech/static_src/src/app/src/redux/actions/submissions.js
index 62091b71d15248937338875529347abbb178812f..11ff549a44b1f7066e1baa303c59aa8287d46871 100644
--- a/opentech/static_src/src/app/src/redux/actions/submissions.js
+++ b/opentech/static_src/src/app/src/redux/actions/submissions.js
@@ -1,3 +1,5 @@
+import { CALL_API } from '@middleware/api'
+
 import api from '@api';
 import {
     getCurrentSubmission,
@@ -63,80 +65,29 @@ export const loadCurrentRoundSubmissions = () => (dispatch, getState) => {
 }
 
 
-export const fetchRound = roundID => {
-    return async function(dispatch) {
-        dispatch(startLoadingRound(roundID));
-        try {
-            const response = await api.fetchRound(roundID);
-            const json = await response.json();
-            if (response.ok) {
-                dispatch(updateRound(roundID, json));
-            } else {
-                dispatch(failLoadingRound(json.detail));
-            }
-        } catch (e) {
-            dispatch(failLoadingRound(e.message));
-        }
-    };
-};
-
-
-const updateRound = (roundID, data) => ({
-    type: UPDATE_ROUND,
-    roundID,
-    data,
-});
-
-
-const startLoadingRound = (roundID) => ({
-    type: START_LOADING_ROUND,
-    roundID,
-});
-
-
-const failLoadingRound = (message) => ({
-    type: FAIL_LOADING_ROUND,
-    message,
-});
-
-
-
-export const fetchSubmissionsByRound = roundID => {
-    return async function(dispatch) {
-        dispatch(startLoadingSubmissionsByRound(roundID));
-        try {
-            const response = await api.fetchSubmissionsByRound(roundID);
-            const json = await response.json();
-            if (response.ok) {
-                dispatch(updateSubmissionsByRound(roundID, json));
-            } else {
-                dispatch(failLoadingSubmissionsByRound(json.detail));
-            }
-        } catch (e) {
-            dispatch(failLoadingSubmissionsByRound(e.message));
-        }
-    };
-};
-
-
-const updateSubmissionsByRound = (roundID, data) => ({
-    type: UPDATE_SUBMISSIONS_BY_ROUND,
+const fetchRound = (roundID) => ({
+    [CALL_API]: {
+        types: [ START_LOADING_ROUND, UPDATE_ROUND, FAIL_LOADING_ROUND],
+        endpoint: api.fetchRound(roundID),
+    },
     roundID,
-    data,
-});
+})
 
-
-const startLoadingSubmissionsByRound = (roundID) => ({
-    type: START_LOADING_SUBMISSIONS_BY_ROUND,
+const fetchSubmissionsByRound = (roundID) => ({
+    [CALL_API]: {
+        types: [ START_LOADING_SUBMISSIONS_BY_ROUND, UPDATE_SUBMISSIONS_BY_ROUND, FAIL_LOADING_SUBMISSIONS_BY_ROUND],
+        endpoint: api.fetchSubmissionsByRound(roundID),
+    },
     roundID,
-});
-
-
-const failLoadingSubmissionsByRound = (message) => ({
-    type: FAIL_LOADING_SUBMISSIONS_BY_ROUND,
-    message,
-});
+})
 
+const fetchSubmission = (submissionID) => ({
+    [CALL_API]: {
+        types: [ START_LOADING_SUBMISSION, UPDATE_SUBMISSION, FAIL_LOADING_SUBMISSION],
+        endpoint: api.fetchSubmission(submissionID),
+    },
+    submissionID,
+})
 
 export const loadCurrentSubmission = (requiredFields=[]) => (dispatch, getState) => {
     const submissionID = getCurrentSubmissionID(getState())
@@ -153,42 +104,6 @@ export const loadCurrentSubmission = (requiredFields=[]) => (dispatch, getState)
 }
 
 
-export const fetchSubmission = submissionID => {
-    return async function(dispatch) {
-
-        dispatch(startLoadingSubmission(submissionID));
-        try {
-            const response = await api.fetchSubmission(submissionID);
-            const json = await response.json();
-            if (response.ok) {
-                dispatch(updateSubmission(submissionID, json));
-            } else {
-                dispatch(failLoadingSubmission(json.detail));
-            }
-        } catch (e) {
-            dispatch(failLoadingSubmission(e.message));
-        }
-    };
-};
-
-
-const startLoadingSubmission = submissionID => ({
-    type: START_LOADING_SUBMISSION,
-    submissionID,
-});
-
-const failLoadingSubmission = submissionID => ({
-    type: FAIL_LOADING_SUBMISSION,
-    submissionID,
-});
-
-
-export const updateSubmission = (submissionID, data) => ({
-    type: UPDATE_SUBMISSION,
-    submissionID,
-    data,
-});
-
 export const clearCurrentSubmission = () => ({
     type: CLEAR_CURRENT_SUBMISSION,
 });
diff --git a/opentech/static_src/src/app/src/redux/middleware/api.js b/opentech/static_src/src/app/src/redux/middleware/api.js
new file mode 100644
index 0000000000000000000000000000000000000000..d808652d5eb396f47f3a0e15177e5ac6b857d93e
--- /dev/null
+++ b/opentech/static_src/src/app/src/redux/middleware/api.js
@@ -0,0 +1,80 @@
+import { camelizeKeys, decamelizeKeys } from 'humps'
+
+import { apiFetch } from '@api/utils'
+
+const callApi = (endpoint) => {
+    // If body is an object, decamelize the keys.
+    const { options } = endpoint;
+    if (options !== undefined && typeof options.body === 'object') {
+        endpoint = {
+            ...endpoint,
+            options: {
+                ...options,
+                body: JSON.stringify(decamelizeKeys(options.body))
+            }
+        }
+    }
+
+    return apiFetch(endpoint)
+        .then(response =>
+              response.json().then(json => {
+                  if (!response.ok) {
+                      return Promise.reject({message: json.error})
+                  }
+                  return camelizeKeys(json)
+              })
+             )
+}
+
+
+export const CALL_API = 'Call API'
+
+
+// A Redux middleware that interprets actions with CALL_API info specified.
+// Performs the call and promises when such actions are dispatched.
+export default store => next => action => {
+    const callAPI = action[CALL_API]
+
+    if (callAPI === undefined) {
+        return next(action)
+    }
+
+    let { endpoint } = callAPI
+    const { types } = callAPI
+
+    if (typeof endpoint === 'function') {
+        endpoint = endpoint(store.getState())
+    }
+
+    if (typeof endpoint !== 'object' && typeof endpoint.path !== 'string') {
+        throw new Error('Specify a string endpoint URL.')
+    }
+
+    if (!Array.isArray(types) || types.length !== 3) {
+        throw new Error('Expected an array of three action types.')
+
+    }
+    if (!types.every(type => typeof type === 'string')) {
+        throw new Error('Expected action types to be strings.')
+    }
+
+    const actionWith = data => {
+        const finalAction = {...action, ...data}
+        delete finalAction[CALL_API]
+        return finalAction
+    }
+
+    const [ requestType, successType, failureType ] = types
+    next(actionWith({ type: requestType }))
+
+    return callApi(endpoint).then(
+        response => next(actionWith({
+            data: response,
+            type: successType
+        })),
+        error => next(actionWith({
+            type: failureType,
+            error: error.message || 'Something bad happened'
+        }))
+  )
+}
diff --git a/opentech/static_src/src/app/src/redux/reducers/submissions.js b/opentech/static_src/src/app/src/redux/reducers/submissions.js
index 0c5253fd60552f4e909819a19728c3a2ca36e7f1..6e66e225a39375232849ec2610b9a4a33b9dd6a3 100644
--- a/opentech/static_src/src/app/src/redux/reducers/submissions.js
+++ b/opentech/static_src/src/app/src/redux/reducers/submissions.js
@@ -7,9 +7,10 @@ import {
     UPDATE_SUBMISSIONS_BY_ROUND,
     UPDATE_SUBMISSION,
     SET_CURRENT_SUBMISSION,
-    ADD_NOTE_FOR_SUBMISSION,
 } from '@actions/submissions';
 
+import { UPDATE_NOTES, UPDATE_NOTE } from '@actions/notes'
+
 
 function submission(state, action) {
     switch(action.type) {
@@ -32,11 +33,16 @@ function submission(state, action) {
                 isFetching: false,
                 isErrored: false,
             };
-        case ADD_NOTE_FOR_SUBMISSION:
+        case UPDATE_NOTES:
+            return {
+                ...state,
+                comments: action.data.results.map(note => note.id),
+            };
+        case UPDATE_NOTE:
             return {
                 ...state,
                 comments: [
-                    action.noteID,
+                    action.data.id,
                     ...(state.comments || []),
                 ]
             };
@@ -50,8 +56,9 @@ function submissionsByID(state = {}, action) {
     switch(action.type) {
         case START_LOADING_SUBMISSION:
         case FAIL_LOADING_SUBMISSION:
-        case ADD_NOTE_FOR_SUBMISSION:
         case UPDATE_SUBMISSION:
+        case UPDATE_NOTE:
+        case UPDATE_NOTES:
             return {
                 ...state,
                 [action.submissionID]: submission(state[action.submissionID], action),
diff --git a/opentech/static_src/src/app/src/redux/store.js b/opentech/static_src/src/app/src/redux/store.js
index ed0b719d6c0ff1968140fc66689ce15d53ddaa07..a7002461574d3319ee31b524868b725189d11b00 100644
--- a/opentech/static_src/src/app/src/redux/store.js
+++ b/opentech/static_src/src/app/src/redux/store.js
@@ -4,9 +4,11 @@ import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'
 import logger from 'redux-logger'
 
 import rootReducer from '@reducers';
+import api from '@middleware/api'
 
 const MIDDLEWARE = [
     ReduxThunk,
+    api,
 ];
 
 if (process.env.NODE_ENV === 'development') {
diff --git a/opentech/static_src/src/app/webpack.base.config.js b/opentech/static_src/src/app/webpack.base.config.js
index b30d026d86c698c658126352f7bc9fd8c41fcca8..3c0f73b6bd702ce544c39510ac7f7d116e15730e 100644
--- a/opentech/static_src/src/app/webpack.base.config.js
+++ b/opentech/static_src/src/app/webpack.base.config.js
@@ -71,6 +71,7 @@ module.exports = {
             '@reducers': path.resolve(__dirname, 'src/redux/reducers'),
             '@selectors': path.resolve(__dirname, 'src/redux/selectors'),
             '@actions': path.resolve(__dirname, 'src/redux/actions'),
+            '@middleware': path.resolve(__dirname, 'src/redux/middleware'),
             '@api': path.resolve(__dirname, 'src/api'),
         }
     }
diff --git a/package-lock.json b/package-lock.json
index acafab1431a4125fa65640316ce03a3ecc34a885..6553c6995b6f055d9adc352016f2535403329d69 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5824,6 +5824,11 @@
             "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
             "dev": true
         },
+        "humps": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz",
+            "integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao="
+        },
         "iconv-lite": {
             "version": "0.4.24",
             "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
diff --git a/package.json b/package.json
index 5757f29e394fe322874f8c8f567b4f1bbcb6871f..88b522444675e5436f6ad83576404ee6853ff28e 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
         "gulp-size": "^3.0.0",
         "gulp-touch-cmd": "0.0.1",
         "gulp-uglify": "^3.0.1",
+        "humps": "^2.0.1",
         "js-cookie": "^2.2.0",
         "markdown": "^0.5.0",
         "moment": "^2.24.0",