diff --git a/bin/keysigner b/bin/keysigner
index 1d981b0c5bc2203a46673e15eef3f423920402de..8e1f2a733bc38ec6e6b35c5c56b473664f8759fb 100755
--- a/bin/keysigner
+++ b/bin/keysigner
@@ -74,13 +74,15 @@ try {
   process.exit(1);
 }
 
+
+
 // and our single function
-app.post('/wsapi/cert_key', validate(["email", "pubkey", "ephemeral"]), function(req, resp) {
+app.post('/wsapi/cert_key', validate({ 'email': 'email', 'pubkey': 'pubkey', 'ephemeral': 'boolean' }), function(req, resp) {
   var startTime = new Date();
   cc.enqueue({
-    pubkey: req.body.pubkey,
-    email: req.body.email,
-    validityPeriod: (req.body.ephemeral ? config.get('ephemeral_session_duration_ms') : config.get('certificate_validity_ms')),
+    pubkey: req.params.pubkey,
+    email: req.params.email,
+    validityPeriod: (req.params.ephemeral ? config.get('ephemeral_session_duration_ms') : config.get('certificate_validity_ms')),
     hostname: HOSTNAME
   }, function (err, r) {
     var reqTime = new Date - startTime;
diff --git a/lib/sanitize.js b/lib/sanitize.js
deleted file mode 100644
index cd63a569f19352ca238ca5e03266db6ad010a8c0..0000000000000000000000000000000000000000
--- a/lib/sanitize.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// a teensy tinsy module to do parameter sanitization.  A good candiate for future
-// librification.
-//
-// usage:
-//
-//   const sanitize = require('sanitize');
-//
-//   sanitize(value).isEmail();
-//   sanitize(value).isDomain();
-
-// XXX - should review these simple regexps
-
-var logger = require('./logging.js').logger;
-
-module.exports = function (value) {
-  var isEmail = function() {
-
-    if (!value.toLowerCase().match(/^[\w.!#$%&'*+\-/=?\^`{|}~]+@[a-z\d_-]+(\.[a-z\d_-]+)+$/i))
-      throw "not a valid email";
-  };
-
-  var isDomain = function() {
-    if (!value.match(/^[a-z\d_-]+(\.[a-z\d-]+)+$/i)) {
-      throw "not a valid domain";
-    }
-  };
-
-  var isOrigin = function() {
-    // allow single hostnames, e.g. localhost
-    if (!value.match(/^https?:\/\/[a-z\d_-]+(\.[a-z\d_-]+)*(:\d+)?$/i)) {
-      throw "not a valid origin";
-    }
-  };
-
-  return {
-    isEmail: isEmail,
-    isDomain: isDomain,
-    isOrigin: isOrigin
-  };
-};
diff --git a/lib/validate.js b/lib/validate.js
index 83b32ac1b3cea754a61f38b06f193d39e63cbafd..dd2e55b6fefb0a8c36668f404d93958dcbc9cd99 100644
--- a/lib/validate.js
+++ b/lib/validate.js
@@ -14,29 +14,104 @@
 
 const
 logger = require('./logging.js').logger,
-httputils = require('./httputils.js');
+httputils = require('./httputils.js'),
+check = require('validator').check;
+
+var types = {
+  email: function(x) {
+    check(x).isEmail();
+  },
+  password: function(x) {
+    check(x).len(8,80);
+  },
+  boolean: function(x) {
+    if (typeof x !== 'boolean') throw "boolean required";
+  },
+  token: function(x) {
+    check(x).len(48,48).isAlphanumeric();
+  },
+  assertion: function(x) {
+    check(x).len(50,10240).regex(/[0-9a-zA-Z~_-]+/);
+  },
+  pubkey: function(x) {
+    check(x).len(50,10240);
+    JSON.parse(x);
+  },
+  origin: function(x) {
+    // allow single hostnames, e.g. localhost
+    if (typeof x !== 'string' || !x.match(/^https?:\/\/[a-z\d_-]+(\.[a-z\d_-]+)*(:\d+)?$/i)) {
+      throw "not a valid origin";
+    }
+  }
+};
 
 module.exports = function (params) {
+  // normalize the parameters description, verify all specified types are present
+  if (Array.isArray(params) || typeof params !== 'object' || typeof params === null) {
+    throw "argument to validate must be an object, not a " + (typeof params);
+  }
+
+  Object.keys(params).forEach(function(p) {
+    var v = params[p];
+    if (typeof v === 'string') {
+      v = { type: v };
+    }
+    if (typeof v.required === "undefined") v.required = true;
+
+    if (!types[v.type]) throw "unknown type specified in WSAPI:" + v.type;
+    params[p] = v;
+  });
+
   return function(req, resp, next) {
-    var params_in_request=null;
+    var reqParams = null;
     if (req.method === "POST") {
-      params_in_request = req.body;
+      reqParams = req.body;
     } else {
-      params_in_request = req.query;
+      reqParams = req.query;
     }
 
+    // clear body and query to prevent wsapi handlers from accessing
+    // un-validated input parameters
+    req.body = {};
+    req.query = {};
+    req.params = {};
+
+    // now validate
     try {
-      params.forEach(function(k) {
-        if (!params_in_request || !params_in_request.hasOwnProperty(k)) {
-          throw k;
+      // allow csrf through
+      if (reqParams.csrf) {
+        req.params.csrf = reqParams.csrf;
+        delete reqParams.csrf;
+      }
+
+      Object.keys(params).forEach(function(p) {
+        if (params[p].required && !reqParams.hasOwnProperty(p)) throw "missing required parameter: '" + p + "'";
+        if (reqParams[p] === undefined) return;
+
+        // validate
+        try {
+          types[params[p].type](reqParams[p]);
+        } catch (e) {
+          throw p + ": " + e.toString();
         }
+        req.params[p] = reqParams[p];
+        delete reqParams[p];
       });
+
+      // if there are any keys left in reqParams, they're not allowable!
+      var extra = Object.keys(reqParams);
+      if (extra.length) throw "extra parameters are not allowed: " + extra.join(', ');
     } catch(e) {
-      var msg = "missing '" + e + "' argument";
-      logger.warn("bad request received: " + msg);
-      return httputils.badRequest(resp, msg);
+      var msg = {
+        success: false,
+        reason: e.toString()
+      };
+      logger.warn("bad request received: " + msg.reason);
+      resp.statusCode = 400;
+      return resp.json(msg);
     }
 
+
     // this is called outside the try/catch because errors
     // in the handling of the request should be caught separately
     next();
diff --git a/lib/wsapi.js b/lib/wsapi.js
index b9c5b50f7f384ec1f2fe5336cc165b27c4441585..cabedf038c27df25dc4dcb4f6b0eb765b8f8e8a7 100644
--- a/lib/wsapi.js
+++ b/lib/wsapi.js
@@ -97,12 +97,6 @@ function authenticateSession(session, uid, level, duration_ms) {
   }
 }
 
-function checkPassword(pass) {
-  if (!pass || pass.length < 8 || pass.length > 80) {
-    return "valid passwords are between 8 and 80 chars";
-  }
-}
-
 function langContext(req) {
   return {
     lang: req.lang,
@@ -147,7 +141,6 @@ exports.clearAuthenticatedUser = clearAuthenticatedUser;
 exports.isAuthed = isAuthed;
 exports.bcryptPassword = bcryptPassword;
 exports.authenticateSession = authenticateSession;
-exports.checkPassword = checkPassword;
 exports.forwardWritesTo = undefined;
 exports.langContext = langContext;
 exports.databaseDown = databaseDown;
@@ -248,7 +241,8 @@ exports.setup = function(options, app) {
     str += op.method.toUpperCase() + " - ";
     str += (op.authed ? "" : "not ") + "authed";
     if (op.args) {
-      str += " - " + op.args.join(", ");
+      var keys = Array.isArray(op.args) ? op.args : Object.keys(op.args);
+      str += " - " + keys.join(", ");
     }
     if (op.internal) str += ' - internal';
     str += ")";
@@ -271,7 +265,6 @@ exports.setup = function(options, app) {
 
       // set up the argument validator
       if (api.args) {
-        if (!Array.isArray(api.args)) throw "exports.args must be an array of strings";
         wsapis[operation].validate = validate(api.args);
       } else {
         wsapis[operation].validate = function(req,res,next) { next(); };
diff --git a/lib/wsapi/add_email_with_assertion.js b/lib/wsapi/add_email_with_assertion.js
index e8649ceb64911cfc659eefc2ae44c5da639d9980..344c9d50d1e0a2680543e015ffccef4b243eff3f 100644
--- a/lib/wsapi/add_email_with_assertion.js
+++ b/lib/wsapi/add_email_with_assertion.js
@@ -15,7 +15,9 @@ https = require('https');
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = 'assertion';
-exports.args = ['assertion'];
+exports.args = {
+  'assertion': 'assertion'
+};
 exports.i18n = false;
 
 // This WSAPI will be invoked when a user attempts to add a primary
@@ -23,7 +25,7 @@ exports.i18n = false;
 // authenticated.
 exports.process = function(req, res) {
   // first let's verify that the assertion is valid
-  primary.verifyAssertion(req.body.assertion, function(err, email) {
+  primary.verifyAssertion(req.params.assertion, function(err, email) {
     if (err) {
       return res.json({
         success: false,
diff --git a/lib/wsapi/address_info.js b/lib/wsapi/address_info.js
index 16943822da8268103daff7b3235f324a278e3cbb..a9ae6733af46cbdbc1754019303459d3f7219198 100644
--- a/lib/wsapi/address_info.js
+++ b/lib/wsapi/address_info.js
@@ -21,19 +21,19 @@ logger = require('../logging.js').logger;
 exports.method = 'get';
 exports.writes_db = false;
 exports.authed = false;
-exports.args = ['email'];
+exports.args = {
+  'email': 'email'
+};
 exports.i18n = false;
 
 const emailRegex = /\@(.*)$/;
 
 exports.process = function(req, res) {
   // parse out the domain from the email
-  var email = url.parse(req.url, true).query['email'];
-  var m = emailRegex.exec(email);
-  if (!m) {
-    return httputils.badRequest(res, "invalid email address");
-  }
+  var m = emailRegex.exec(req.params.email);
+
   // Saftey value for production branch only
+  // (lth) ^^ what does this mean? ^^
   var done = false;
   primary.checkSupport(m[1], function(err, urls, publicKey, delegates) {
     if (done) {
@@ -49,7 +49,7 @@ exports.process = function(req, res) {
       urls.type = 'primary';
       res.json(urls);
     } else {
-      db.emailKnown(email, function(err, known) {
+      db.emailKnown(req.params.email, function(err, known) {
         if (err) {
           return wsapi.databaseDown(res, err);
         } else {
diff --git a/lib/wsapi/auth_with_assertion.js b/lib/wsapi/auth_with_assertion.js
index 37127d6edab08d8c01cfafc8edc7e2f8dd76a620..3d76fa43a00e40f437838afa14af4403c3d81965 100644
--- a/lib/wsapi/auth_with_assertion.js
+++ b/lib/wsapi/auth_with_assertion.js
@@ -16,7 +16,10 @@ config = require('../configuration');
 exports.method = 'post';
 exports.writes_db = false;
 exports.authed = false;
-exports.args = ['assertion', 'ephemeral'];
+exports.args = {
+  'assertion': 'assertion',
+  'ephemeral': 'boolean'
+};
 exports.i18n = false;
 
 exports.process = function(req, res) {
@@ -25,7 +28,7 @@ exports.process = function(req, res) {
   // create a user account if that's needed
 
   // 1. first let's verify that the assertion is valid
-  primary.verifyAssertion(req.body.assertion, function(err, email) {
+  primary.verifyAssertion(req.params.assertion, function(err, email) {
     if (err) {
       return res.json({
         success: false,
@@ -43,7 +46,7 @@ exports.process = function(req, res) {
           if (err) return wsapi.databaseDown(res, err);
           if (!uid) return res.json({ success: false, reason: "internal error" });
           wsapi.authenticateSession(req.session, uid, 'assertion',
-                                    req.body.ephemeral ? config.get('ephemeral_session_duration_ms')
+                                    req.params.ephemeral ? config.get('ephemeral_session_duration_ms')
                                                        : config.get('authentication_duration_ms'));
           return res.json({ success: true, userid: uid });
         });
@@ -61,8 +64,8 @@ exports.process = function(req, res) {
       var m = u.scheme === 'http' ? http : https;
 
       var post_body = querystring.stringify({
-        assertion: req.body.assertion,
-        csrf: req.body.csrf
+        assertion: req.params.assertion,
+        csrf: req.params.csrf
       });
 
       var preq = m.request({
@@ -94,7 +97,7 @@ exports.process = function(req, res) {
 
           logger.info("successfully created primary acct for " + email + " (" + r.userid + ")");
           wsapi.authenticateSession(req.session, r.userid, 'assertion',
-                                    req.body.ephemeral ? config.get('ephemeral_session_duration_ms')
+                                    req.params.ephemeral ? config.get('ephemeral_session_duration_ms')
                                                        : config.get('authentication_duration_ms'));
           res.json({ success: true, userid: r.userid });
         });
diff --git a/lib/wsapi/authenticate_user.js b/lib/wsapi/authenticate_user.js
index 938b38aa152b275b0ad489b08d1bb81edf24bb6d..6c0d21db8c3e99a115d6874d1c9d2871a6c8c727 100644
--- a/lib/wsapi/authenticate_user.js
+++ b/lib/wsapi/authenticate_user.js
@@ -17,18 +17,22 @@ config = require('../configuration');
 exports.method = 'post';
 exports.writes_db = false;
 exports.authed = false;
-exports.args = ['email','pass', 'ephemeral'];
 exports.i18n = false;
+exports.args = {
+  'email': 'email',
+  'pass':  'password',
+  'ephemeral': 'boolean'
+};
 
 exports.process = function(req, res) {
   function fail(reason) {
     var r = { success: false };
     if (reason) r.reason = reason;
-    logger.debug('authentication fails for user: ' + req.body.email + (reason ? (' - ' + reason) : ""));
+    logger.debug('authentication fails for user: ' + req.params.email + (reason ? (' - ' + reason) : ""));
     return res.json(r);
   }
 
-  db.emailToUID(req.body.email, function(err, uid) {
+  db.emailToUID(req.params.email, function(err, uid) {
     if (err) return wsapi.databaseDown(res, err);
 
     if (typeof uid !== 'number') {
@@ -43,7 +47,7 @@ exports.process = function(req, res) {
       }
 
       var startTime = new Date();
-      bcrypt.compare(req.body.pass, hash, function (err, success) {
+      bcrypt.compare(req.params.pass, hash, function (err, success) {
         var reqTime = new Date - startTime;
         statsd.timing('bcrypt.compare_time', reqTime);
 
@@ -56,13 +60,13 @@ exports.process = function(req, res) {
           logger.error("error comparing passwords with bcrypt: " + err);
           return fail("internal password check error");
         } else if (!success) {
-          return fail("password mismatch for user: " + req.body.email);
+          return fail("password mismatch for user: " + req.params.email);
         } else {
           if (!req.session) req.session = {};
 
           wsapi.authenticateSession(req.session, uid, 'password',
-                                    req.body.ephemeral ? config.get('ephemeral_session_duration_ms')
-                                                       : config.get('authentication_duration_ms'));
+                                    req.params.ephemeral ? config.get('ephemeral_session_duration_ms')
+                                                         : config.get('authentication_duration_ms'));
           res.json({ success: true, userid: uid });
 
 
@@ -78,9 +82,9 @@ exports.process = function(req, res) {
             var m = u.scheme === 'http' ? http : https;
 
             var post_body = querystring.stringify({
-              oldpass: req.body.pass,
-              newpass: req.body.pass,
-              csrf: req.body.csrf
+              oldpass: req.params.pass,
+              newpass: req.params.pass,
+              csrf: req.params.csrf
             });
             var preq = m.request({
               host: u.host,
diff --git a/lib/wsapi/cert_key.js b/lib/wsapi/cert_key.js
index 1f373b92a0a5377351cf2d56619d8eef1ed4e694..71be5c629cfb815dc2f6df14a9462d902f983792 100644
--- a/lib/wsapi/cert_key.js
+++ b/lib/wsapi/cert_key.js
@@ -14,11 +14,15 @@ wsapi = require('../wsapi.js');
 exports.method = 'post';
 exports.writes_db = false;
 exports.authed = 'password';
-exports.args = ['email','pubkey','ephemeral'];
+exports.args = {
+  'email': 'email',
+  'pubkey': 'pubkey',
+  'ephemeral': 'boolean'
+};
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  db.userOwnsEmail(req.session.userid, req.body.email, function(err, owned) {
+  db.userOwnsEmail(req.session.userid, req.params.email, function(err, owned) {
     if (err) return wsapi.databaseDown(res, err);
 
     // not same account? big fat error
@@ -27,12 +31,22 @@ exports.process = function(req, res) {
     // secondary addresses in the database may be "unverified".  this occurs when
     // a user forgets their password.  We will not issue certs for unverified email
     // addresses
-    db.emailIsVerified(req.body.email, function(err, verified) {
+    db.emailIsVerified(req.params.email, function(err, verified) {
       if (!verified) return httputils.forbidden(res, "that email requires (re)verification");
 
       // forward to the keysigner!
       var keysigner = urlparse(config.get('keysigner_url'));
       keysigner.path = '/wsapi/cert_key';
+
+      // parameter validation moves arguments from req.body to req.params,
+      // and removes them from req.body.  This feature makes it impossible
+      // to use unvalidated params in your wsapi "process" function.
+      // 
+      // http_forward, however, will only forward params in req.body
+      // or req.query.  so we explicitly copy req.params to req.body
+      // to cause them to be forwarded.
+      req.body = req.params;
+
       forward(keysigner, req, res, function(err) {
         if (err) {
           logger.error("error forwarding request to keysigner: " + err);
diff --git a/lib/wsapi/complete_email_confirmation.js b/lib/wsapi/complete_email_confirmation.js
index fefdd1fc31d6fd9da4f76aadf908f6cfa6441165..f3ba86e791dadd98de37b3ef7e2ccba68ff205ed 100644
--- a/lib/wsapi/complete_email_confirmation.js
+++ b/lib/wsapi/complete_email_confirmation.js
@@ -18,9 +18,15 @@ httputils = require('../httputils.js');
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = false;
-// NOTE: this API also takes a 'pass' parameter which is required
-// when a user has a null password (only primaries on their acct)
-exports.args = ['token'];
+exports.args = {
+  'token': 'token',
+  // NOTE: 'pass' is required when a user has a null password
+  // (only primaries on their acct)
+  'pass': {
+    type: 'password',
+    required: false
+  }
+};
 exports.i18n = false;
 
 exports.process = function(req, res) {
@@ -28,7 +34,8 @@ exports.process = function(req, res) {
   //
   // 1. you must already be authenticated as the user who initiated the verification
   // 2. you must provide the password of the initiator.
-  db.authForVerificationSecret(req.body.token, function(err, initiator_hash, initiator_uid) {
+
+  db.authForVerificationSecret(req.params.token, function(err, initiator_hash, initiator_uid) {
     if (err) {
       logger.info("unknown verification secret: " + err);
       return wsapi.databaseDown(res, err);
@@ -36,8 +43,8 @@ exports.process = function(req, res) {
 
     if (req.session.userid === initiator_uid) {
       postAuthentication();
-    } else if (typeof req.body.pass === 'string') {
-      bcrypt.compare(req.body.pass, initiator_hash, function (err, success) {
+    } else if (typeof req.params.pass === 'string') {
+      bcrypt.compare(req.params.pass, initiator_hash, function (err, success) {
         if (err) {
           logger.warn("max load hit, failing on auth request with 503: " + err);
           return httputils.serviceUnavailable(res, "server is too busy");
@@ -52,7 +59,7 @@ exports.process = function(req, res) {
     }
 
     function postAuthentication() {
-      db.completeConfirmEmail(req.body.token, function(e, email, uid) {
+      db.completeConfirmEmail(req.params.token, function(e, email, uid) {
         if (e) {
           logger.warn("couldn't complete email verification: " + e);
           wsapi.databaseDown(res, e);
diff --git a/lib/wsapi/complete_reset.js b/lib/wsapi/complete_reset.js
index b48f6582e94814fa91c730f4e68c760a6c1b4ae2..4d3bcfec2b762c39d535d8938f9b36e37e2fecba 100644
--- a/lib/wsapi/complete_reset.js
+++ b/lib/wsapi/complete_reset.js
@@ -15,7 +15,13 @@ exports.writes_db = true;
 exports.authed = false;
 // NOTE: this API also takes a 'pass' parameter which is required
 // when a user has a null password (only primaries on their acct)
-exports.args = ['token'];
+exports.args = {
+  'token': 'token',
+  'pass': {
+    type: 'password',
+    required: false
+  }
+};
 exports.i18n = true;
 
 exports.process = function(req, res) {
@@ -28,18 +34,18 @@ exports.process = function(req, res) {
 
   // is this the same browser?
   if (typeof req.session.pendingReset === 'string' &&
-      req.body.token === req.session.pendingReset) {
+      req.params.token === req.session.pendingReset) {
     return postAuthentication();
   }
   // is a password provided?
-  else if (typeof req.body.pass === 'string') {
-    return db.authForVerificationSecret(req.body.token, function(err, hash) {
+  else if (typeof req.params.pass === 'string') {
+    return db.authForVerificationSecret(req.params.token, function(err, hash) {
       if (err) {
         logger.warn("couldn't get password for verification secret: " + err);
         return wsapi.databaseDown(res, err);
       }
 
-      bcrypt.compare(req.body.pass, hash, function (err, success) {
+      bcrypt.compare(req.params.pass, hash, function (err, success) {
         if (err) {
           logger.warn("max load hit, failing on auth request with 503: " + err);
           return httputils.serviceUnavailable(res, "server is too busy");
@@ -55,7 +61,7 @@ exports.process = function(req, res) {
   }
 
   function postAuthentication() {
-    db.haveVerificationSecret(req.body.token, function(err, known) {
+    db.haveVerificationSecret(req.params.token, function(err, known) {
       if (err) return wsapi.databaseDown(res, err);
 
       if (!known) {
@@ -65,7 +71,7 @@ exports.process = function(req, res) {
         return res.json({ success: false} );
       }
 
-      db.completePasswordReset(req.body.token, function(err, email, uid) {
+      db.completePasswordReset(req.params.token, function(err, email, uid) {
         if (err) {
           logger.warn("couldn't complete email verification: " + err);
           wsapi.databaseDown(res, err);
diff --git a/lib/wsapi/complete_user_creation.js b/lib/wsapi/complete_user_creation.js
index 5e253d38a5f7ca5b766796c6dcdab5936e07adf3..f737e3b8f297bc8873070dcc4b2a2d905b6e9fd4 100644
--- a/lib/wsapi/complete_user_creation.js
+++ b/lib/wsapi/complete_user_creation.js
@@ -13,7 +13,15 @@ config = require('../configuration');
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = false;
-exports.args = ['token'];
+exports.args = {
+  'token': 'token',
+  // NOTE: 'pass' is required when a user completes on a different device
+  // than they initiate
+  'pass': {
+    type: 'password',
+    required: false
+  }
+};
 exports.i18n = false;
 
 exports.process = function(req, res) {
@@ -31,18 +39,18 @@ exports.process = function(req, res) {
 
   // is this the same browser?
   if (typeof req.session.pendingCreation === 'string' &&
-      req.body.token === req.session.pendingCreation) {
+      req.params.token === req.session.pendingCreation) {
     return postAuthentication();
   }
   // is a password provided?
-  else if (typeof req.body.pass === 'string') {
-    return db.authForVerificationSecret(req.body.token, function(err, hash) {
+  else if (typeof req.params.pass === 'string') {
+    return db.authForVerificationSecret(req.params.token, function(err, hash) {
       if (err) {
         logger.warn("couldn't get password for verification secret: " + err);
         return wsapi.databaseDown(res, err);
       }
 
-      bcrypt.compare(req.body.pass, hash, function (err, success) {
+      bcrypt.compare(req.params.pass, hash, function (err, success) {
         if (err) {
           logger.warn("max load hit, failing on auth request with 503: " + err);
           return httputils.serviceUnavailable(res, "server is too busy");
@@ -58,7 +66,7 @@ exports.process = function(req, res) {
   }
 
   function postAuthentication() {
-    db.haveVerificationSecret(req.body.token, function(err, known) {
+    db.haveVerificationSecret(req.params.token, function(err, known) {
       if (err) return wsapi.databaseDown(res, err);
 
       if (!known) {
@@ -68,7 +76,7 @@ exports.process = function(req, res) {
         return res.json({ success: false} );
       }
 
-      db.completeCreateUser(req.body.token, function(err, email, uid) {
+      db.completeCreateUser(req.params.token, function(err, email, uid) {
         if (err) {
           logger.warn("couldn't complete email verification: " + err);
           wsapi.databaseDown(res, err);
diff --git a/lib/wsapi/create_account_with_assertion.js b/lib/wsapi/create_account_with_assertion.js
index 3fdd3f3608037056ab8326767946d58a91ef182e..96beb7d9b225f76641d0547e99a3473757d64586 100644
--- a/lib/wsapi/create_account_with_assertion.js
+++ b/lib/wsapi/create_account_with_assertion.js
@@ -13,16 +13,20 @@ exports.method = 'post';
 exports.writes_db = true;
 exports.authed = false;
 exports.internal = true;
-exports.args = ['assertion'];
+exports.args = {
+  assertion: 'assertion'
+};
 exports.i18n = false;
 
 exports.process = function(req, res) {
   // let's (re)verify that the assertion is valid
-  primary.verifyAssertion(req.body.assertion, function(err, email) {
+  primary.verifyAssertion(req.params.assertion, function(err, email) {
     if (err) {
       // this should not be an error, the assertion should have already been
       // tested on the webhead
-      logger.error('verfication of primary assertion failed unexpectedly dbwriter (' + err + '): ' + req.body.assertion);
+      logger.error('verfication of primary assertion failed unexpectedly dbwriter (' + err + '): ' +
+                   req.params.assertion);
+
       return httputils.serverError(res);
     }
 
diff --git a/lib/wsapi/email_addition_status.js b/lib/wsapi/email_addition_status.js
index 5a7a3017d536a095cb2072137e94a6c6fa436021..d3eb3e569215db59e6b1f7fd8f0ace984780f19a 100644
--- a/lib/wsapi/email_addition_status.js
+++ b/lib/wsapi/email_addition_status.js
@@ -15,11 +15,11 @@ wsapi = require('../wsapi.js');
 exports.method = 'get';
 exports.writes_db = false;
 exports.authed = 'assertion';
-exports.args = ['email'];
+exports.args = { email: 'email' };
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  var email = req.query.email;
+  var email = req.params.email;
 
   // check if the currently authenticated user has the email stored under pendingAddition
   // in their acct.
diff --git a/lib/wsapi/email_for_token.js b/lib/wsapi/email_for_token.js
index 41a3c40272fc8c9139433c775373ce1097927d03..3aed92174cf1c5b138638959cffea8a223355803 100644
--- a/lib/wsapi/email_for_token.js
+++ b/lib/wsapi/email_for_token.js
@@ -21,12 +21,14 @@ logger = require('../logging.js').logger;
 exports.method = 'get';
 exports.writes_db = false;
 exports.authed = false;
-exports.args = ['token'];
+exports.args = {
+  'token': 'token'
+};
 exports.i18n = false;
 
 exports.process = function(req, res) {
 
-  db.emailForVerificationSecret(req.query.token, function(err, email, uid, hash) {
+  db.emailForVerificationSecret(req.params.token, function(err, email, uid, hash) {
     if (err) {
       if (err === 'database unavailable') {
         return httputils.serviceUnavailable(res, err);
@@ -48,11 +50,11 @@ exports.process = function(req, res) {
         must_auth = false;
       }
       else if (!uid && typeof req.session.pendingCreation === 'string' &&
-               req.query.token === req.session.pendingCreation) {
+               req.params.token === req.session.pendingCreation) {
         must_auth = false;
       }
       else if (typeof req.session.pendingReset === 'string' &&
-               req.query.token === req.session.pendingReset)
+               req.params.token === req.session.pendingReset)
       {
         must_auth = false;
       }
diff --git a/lib/wsapi/email_reverify_status.js b/lib/wsapi/email_reverify_status.js
index 5068459c3312e7f0fc3baf2e48b5a6a9319f2b1c..c46d887517d99e1622d233a547714d812c8d33df 100644
--- a/lib/wsapi/email_reverify_status.js
+++ b/lib/wsapi/email_reverify_status.js
@@ -13,21 +13,17 @@ wsapi = require('../wsapi.js');
 exports.method = 'get';
 exports.writes_db = false;
 exports.authed = 'assertion';
-exports.args = ['email'];
+exports.args = { email: 'email' };
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  var email = req.query.email;
-  
+
   // For simplicity, all we check is if an email is verified.  We do not check that
   // the email is owned by the currently authenticated user, nor that the verification
   // secret still exists.  These checks would require more database interactions, and
   // other calls will fail in such circumstances.
-
-  // is the address verified?
-  db.emailIsVerified(email, function(err, verified) {
+  db.emailIsVerified(req.params.email, function(err, verified) {
     if (err) return wsapi.databaseDown(res, err);
-
     res.json({ status: verified ? 'complete' : 'pending' });
   });
 };
diff --git a/lib/wsapi/have_email.js b/lib/wsapi/have_email.js
index 2ff22f0306e81331435dcff93cf9107d18322c05..931bbb40b72ef9f0eac0e7458adb04abcf2eebf8 100644
--- a/lib/wsapi/have_email.js
+++ b/lib/wsapi/have_email.js
@@ -12,12 +12,13 @@ url = require('url');
 exports.method = 'get';
 exports.writes_db = false;
 exports.authed = false;
-exports.args = ['email'];
 exports.i18n = false;
+exports.args = {
+  'email': 'email'
+};
 
 exports.process = function(req, res) {
-  var email = url.parse(req.url, true).query['email'];
-  db.emailKnown(email, function(err, known) {
+  db.emailKnown(req.params.email, function(err, known) {
     if (err) return wsapi.databaseDown(res, err);
     res.json({ email_known: known });
   });
diff --git a/lib/wsapi/password_reset_status.js b/lib/wsapi/password_reset_status.js
index 6059eac063238f9507b52c893ea01fc663c19428..e82b2f1dfb6d59dec8e88fe3b9b7412ab95ca6a3 100644
--- a/lib/wsapi/password_reset_status.js
+++ b/lib/wsapi/password_reset_status.js
@@ -6,25 +6,16 @@ const
 db = require('../db.js'),
 wsapi = require('../wsapi.js'),
 logger = require('../logging.js').logger,
-httputils = require('../httputils.js'),
-sanitize = require('../sanitize.js');
+httputils = require('../httputils.js');
 
 exports.method = 'get';
 exports.writes_db = false;
 exports.authed = false;
-exports.args = ['email'];
+exports.args = { email: 'email' };
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  var email = req.query.email;
-
-  try {
-    sanitize(email).isEmail();
-  } catch(e) {
-    var msg = "invalid arguments: " + e;
-    logger.warn("bad request received: " + msg);
-    return httputils.badRequest(res, msg);
-  }
+  var email = req.params.email;
 
   // if the email is in the staged table, we are not complete yet.
   // if the email is not in the staged table -
diff --git a/lib/wsapi/remove_email.js b/lib/wsapi/remove_email.js
index 145adf03deab484899eb96fffacfb55b1d3b0d90..87ae5a7c44ba0e40d2d708ba086e23922558e531 100644
--- a/lib/wsapi/remove_email.js
+++ b/lib/wsapi/remove_email.js
@@ -11,11 +11,13 @@ logger = require('../logging.js').logger;
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = 'assertion';
-exports.args = ['email'];
+exports.args = {
+  'email': 'email'
+};
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  var email = req.body.email;
+  var email = req.params.email;
 
   db.removeEmail(req.session.userid, email, function(error) {
     if (error) {
diff --git a/lib/wsapi/stage_email.js b/lib/wsapi/stage_email.js
index 1e9d8cdb4f7cdad18008714885ff85100b1fc512..859f1323deb18ceb02df4180a4c1504ca9bd1304 100644
--- a/lib/wsapi/stage_email.js
+++ b/lib/wsapi/stage_email.js
@@ -8,7 +8,6 @@ wsapi = require('../wsapi.js'),
 httputils = require('../httputils'),
 logger = require('../logging.js').logger,
 email = require('../email.js'),
-sanitize = require('../sanitize'),
 config = require('../configuration');
 
 /* Stage an email for addition to a user's account.  Causes email to be sent. */
@@ -16,7 +15,14 @@ config = require('../configuration');
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = 'assertion';
-exports.args = ['email','site'];
+exports.args = {
+  email: 'email',
+  site: 'origin',
+  pass: {
+    type: 'password',
+    required: false
+  }
+};
 exports.i18n = true;
 
 exports.process = function(req, res) {
@@ -24,21 +30,11 @@ exports.process = function(req, res) {
   // is currently NULL - this would occur in the case where this is the
   // first secondary address to be added to an account
 
-  // validate
-  try {
-    sanitize(req.body.email).isEmail();
-    sanitize(req.body.site).isOrigin();
-  } catch(e) {
-    var msg = "invalid arguments: " + e;
-    logger.warn("bad request received: " + msg);
-    return httputils.badRequest(res, msg);
-  }
-
-  db.lastStaged(req.body.email, function (err, last) {
+  db.lastStaged(req.params.email, function (err, last) {
     if (err) return wsapi.databaseDown(res, err);
 
     if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
-      logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
+      logger.warn('throttling request to stage email address ' + req.params.email + ', only ' +
                   ((new Date() - last) / 1000.0) + "s elapsed");
       return httputils.throttled(res, "Too many emails sent to that address, try again later.");
     }
@@ -46,13 +42,12 @@ exports.process = function(req, res) {
     db.checkAuth(req.session.userid, function(err, hash) {
       var needs_password = !hash;
 
-      if (!err && needs_password && !req.body.pass) {
+      if (!err && needs_password && !req.params.pass) {
         err = "user must choose a password";
       }
-      if (!err && !needs_password && req.body.pass) {
+      if (!err && !needs_password && req.params.pass) {
         err = "a password may not be set at this time";
       }
-      if (!err && needs_password) err = wsapi.checkPassword(req.body.pass);
 
       if (err) {
         logger.info("stage of email fails: " + err);
@@ -63,7 +58,7 @@ exports.process = function(req, res) {
       }
 
       if (needs_password) {
-        wsapi.bcryptPassword(req.body.pass, function(err, hash) {
+        wsapi.bcryptPassword(req.params.pass, function(err, hash) {
           if (err) {
             logger.warn("couldn't bcrypt password during email verification: " + err);
             return res.json({ success: false });
@@ -78,7 +73,7 @@ exports.process = function(req, res) {
       function completeStage(hash) {
         try {
           // on failure stageEmail may throw
-          db.stageEmail(req.session.userid, req.body.email, hash, function(err, secret) {
+          db.stageEmail(req.session.userid, req.params.email, hash, function(err, secret) {
             if (err) return wsapi.databaseDown(res, err);
 
             var langContext = wsapi.langContext(req);
@@ -88,7 +83,7 @@ exports.process = function(req, res) {
 
             res.json({ success: true });
             // let's now kick out a verification email!
-            email.sendConfirmationEmail(req.body.email, req.body.site, secret, langContext);
+            email.sendConfirmationEmail(req.params.email, req.params.site, secret, langContext);
           });
         } catch(e) {
           // we should differentiate tween' 400 and 500 here.
diff --git a/lib/wsapi/stage_reset.js b/lib/wsapi/stage_reset.js
index a8aefbbcbdba61397c637ad9f41761ac0bb340ec..fda8ee6ba866b0333a4545f93365fd98b3b11e92 100644
--- a/lib/wsapi/stage_reset.js
+++ b/lib/wsapi/stage_reset.js
@@ -8,7 +8,6 @@ wsapi = require('../wsapi.js'),
 httputils = require('../httputils'),
 logger = require('../logging.js').logger,
 email = require('../email.js'),
-sanitize = require('../sanitize'),
 config = require('../configuration');
 
 /* First half of account creation.  Stages a user account for creation.
@@ -20,40 +19,24 @@ config = require('../configuration');
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = false;
-exports.args = ['email','site','pass'];
+exports.args = {
+  email: 'email',
+  site:  'origin',
+  pass:  'password'
+};
 exports.i18n = true;
 
 exports.process = function(req, res) {
-  // a password *must* be supplied to this call iff the user's password
-  // is currently NULL - this would occur in the case where this is the
-  // first secondary address to be added to an account
-
-  // validate
-  try {
-    sanitize(req.body.email).isEmail();
-    sanitize(req.body.site).isOrigin();
-  } catch(e) {
-    var msg = "invalid arguments: " + e;
-    logger.warn("bad request received: " + msg);
-    return httputils.badRequest(res, msg);
-  }
-
-  var err = wsapi.checkPassword(req.body.pass);
-  if (err) {
-    logger.warn("invalid password received: " + err);
-    return httputils.badRequest(res, err);
-  }
-
-  db.lastStaged(req.body.email, function (err, last) {
+  db.lastStaged(req.params.email, function (err, last) {
     if (err) return wsapi.databaseDown(res, err);
 
     if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
-      logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
+      logger.warn('throttling request to stage email address ' + req.params.email + ', only ' +
                   ((new Date() - last) / 1000.0) + "s elapsed");
       return httputils.throttled(res, "Too many emails sent to that address, try again later.");
     }
 
-    db.emailToUID(req.body.email, function(err, uid) {
+    db.emailToUID(req.params.email, function(err, uid) {
       if (err) {
         logger.info("reset password fails: " + err);
         return res.json({ success: false });
@@ -70,7 +53,7 @@ exports.process = function(req, res) {
       wsapi.clearAuthenticatedUser(req.session);
 
       // now bcrypt the password
-      wsapi.bcryptPassword(req.body.pass, function (err, hash) {
+      wsapi.bcryptPassword(req.params.pass, function (err, hash) {
         if (err) {
           if (err.indexOf('exceeded') != -1) {
             logger.warn("max load hit, failing on auth request with 503: " + err);
@@ -82,7 +65,7 @@ exports.process = function(req, res) {
 
         // on failure stageEmail may throw
         try {
-          db.stageEmail(uid, req.body.email, hash, function(err, secret) {
+          db.stageEmail(uid, req.params.email, hash, function(err, secret) {
             if (err) return wsapi.databaseDown(res, err);
 
             var langContext = wsapi.langContext(req);
@@ -93,7 +76,7 @@ exports.process = function(req, res) {
             res.json({ success: true });
 
             // let's now kick out a verification email!
-            email.sendForgotPasswordEmail(req.body.email, req.body.site, secret, langContext);
+            email.sendForgotPasswordEmail(req.params.email, req.params.site, secret, langContext);
           });
         } catch(e) {
           // we should differentiate tween' 400 and 500 here.
diff --git a/lib/wsapi/stage_reverify.js b/lib/wsapi/stage_reverify.js
index 885fcb57ce7c05081dc3c8c06d70ecb16ce0110a..37036c43814b8c788e09a6e50f945db5b3ddc8d0 100644
--- a/lib/wsapi/stage_reverify.js
+++ b/lib/wsapi/stage_reverify.js
@@ -8,7 +8,6 @@ wsapi = require('../wsapi.js'),
 httputils = require('../httputils'),
 logger = require('../logging.js').logger,
 email = require('../email.js'),
-sanitize = require('../sanitize'),
 config = require('../configuration');
 
 /* Stage an email for re-verification (i.e. after account password reset).
@@ -17,37 +16,30 @@ config = require('../configuration');
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = 'assertion';
-exports.args = ['email','site'];
+exports.args = {
+  email: 'email',
+  site: 'origin'
+};
 exports.i18n = true;
 
 exports.process = function(req, res) {
-  // validate
-  try {
-    sanitize(req.body.email).isEmail();
-    sanitize(req.body.site).isOrigin();
-  } catch(e) {
-    var msg = "invalid arguments: " + e;
-    logger.warn("bad request received: " + msg);
-    return httputils.badRequest(res, msg);
-  }
-
   // Note, we do no throttling of emails in this case.  Because this call requires
   // authentication, protect a user from themselves could cause more harm than good,
   // specifically we would be removing a user available workaround (i.e. a cosmic ray
   // hits our email delivery, user doesn't get an email in 30s.  User tries again.)
 
   // one may only reverify an email that is owned and unverified
-  db.userOwnsEmail(req.session.userid, req.body.email, function(err, owned) {
+  db.userOwnsEmail(req.session.userid, req.params.email, function(err, owned) {
     if (err) return res.json({ success: false, reason: err });
     if (!owned) return res.json({ success: false, reason: 'you don\'t control that email address' });
 
-    db.emailIsVerified(req.body.email, function(err, verified) { 
+    db.emailIsVerified(req.params.email, function(err, verified) { 
       if (err) return res.json({ success: false, reason: err });
       if (verified) return res.json({ success: false, reason: 'email is already verified' });
 
       try {
         // on failure stageEmail may throw
-        db.stageEmail(req.session.userid, req.body.email, undefined, function(err, secret) {
+        db.stageEmail(req.session.userid, req.params.email, undefined, function(err, secret) {
           if (err) return wsapi.databaseDown(res, err);
 
           var langContext = wsapi.langContext(req);
@@ -57,7 +49,7 @@ exports.process = function(req, res) {
           
           res.json({ success: true });
           // let's now kick out a verification email!
-          email.sendConfirmationEmail(req.body.email, req.body.site, secret, langContext);
+          email.sendConfirmationEmail(req.params.email, req.params.site, secret, langContext);
         });
       } catch(e) {
         // we should differentiate tween' 400 and 500 here.
diff --git a/lib/wsapi/stage_user.js b/lib/wsapi/stage_user.js
index 8498194ff1b395190c62c32bd281a4b9e620294e..9d3b6c0df1fb9b08ebbf81cb0ce717f488c10f3f 100644
--- a/lib/wsapi/stage_user.js
+++ b/lib/wsapi/stage_user.js
@@ -8,7 +8,6 @@ wsapi = require('../wsapi.js'),
 httputils = require('../httputils'),
 logger = require('../logging.js').logger,
 email = require('../email.js'),
-sanitize = require('../sanitize'),
 config = require('../configuration');
 
 /* First half of account creation.  Stages a user account for creation.
@@ -20,34 +19,21 @@ config = require('../configuration');
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = false;
-exports.args = ['email','pass','site'];
+exports.args = {
+  'email': 'email',
+  'pass': 'password',
+  'site': 'origin'
+};
 exports.i18n = true;
 
 exports.process = function(req, res) {
   var langContext = wsapi.langContext(req);
 
-  // validate
-  try {
-    sanitize(req.body.email).isEmail();
-    sanitize(req.body.site).isOrigin();
-    if(!req.body.pass) throw "missing pass";
-  } catch(e) {
-    var msg = "invalid arguments: " + e;
-    logger.warn("bad request received: " + msg);
-    return httputils.badRequest(res, msg);
-  }
-
-  var err = wsapi.checkPassword(req.body.pass);
-  if (err) {
-    logger.warn("invalid password received: " + err);
-    return httputils.badRequest(res, err);
-  }
-
-  db.lastStaged(req.body.email, function (err, last) {
+  db.lastStaged(req.params.email, function (err, last) {
     if (err) return wsapi.databaseDown(res, err);
 
     if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
-      logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
+      logger.warn('throttling request to stage email address ' + req.params.email + ', only ' +
                   ((new Date() - last) / 1000.0) + "s elapsed");
       return httputils.throttled(res, "Too many emails sent to that address, try again later.");
     }
@@ -56,7 +42,7 @@ exports.process = function(req, res) {
     wsapi.clearAuthenticatedUser(req.session);
 
     // now bcrypt the password
-    wsapi.bcryptPassword(req.body.pass, function (err, hash) {
+    wsapi.bcryptPassword(req.params.pass, function (err, hash) {
       if (err) {
         if (err.indexOf('exceeded') != -1) {
           logger.warn("max load hit, failing on auth request with 503: " + err);
@@ -69,7 +55,7 @@ exports.process = function(req, res) {
       try {
         // upon success, stage_user returns a secret (that'll get baked into a url
         // and given to the user), on failure it throws
-        db.stageUser(req.body.email, hash, function(err, secret) {
+        db.stageUser(req.params.email, hash, function(err, secret) {
           if (err) return wsapi.databaseDown(res, err);
 
           // store the email being registered in the session data
@@ -83,7 +69,7 @@ exports.process = function(req, res) {
           res.json({ success: true });
 
           // let's now kick out a verification email!
-          email.sendNewUserEmail(req.body.email, req.body.site, secret, langContext);
+          email.sendNewUserEmail(req.params.email, req.params.site, secret, langContext);
         });
       } catch(e) {
         // we should differentiate tween' 400 and 500 here.
diff --git a/lib/wsapi/update_password.js b/lib/wsapi/update_password.js
index d7a395c3a49a7cf4d8330bf30b703443ce0fe3de..52471aa05c45d18c5baa67ef2dde104855213395 100644
--- a/lib/wsapi/update_password.js
+++ b/lib/wsapi/update_password.js
@@ -12,27 +12,22 @@ bcrypt = require('../bcrypt');
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = 'password';
-exports.args = ['oldpass','newpass'];
+exports.args = {
+  oldpass: 'password',
+  newpass: 'password'
+};
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  var err = wsapi.checkPassword(req.body.newpass);
-  if (err) {
-    return res.json({
-      success:false,
-      reason: err
-    });
-  }
-
   db.checkAuth(req.session.userid, function(err, hash) {
     if (err) return wsapi.databaseDown(res, err);
 
-    if (typeof hash !== 'string' || typeof req.body.oldpass !== 'string')
+    if (typeof hash !== 'string' || typeof req.params.oldpass !== 'string')
     {
       return res.json({ success: false });
     }
 
-    bcrypt.compare(req.body.oldpass, hash, function (err, success) {
+    bcrypt.compare(req.params.oldpass, hash, function (err, success) {
       if (err) {
         if (err.indexOf('exceeded') != -1) {
           logger.warn("max load hit, failing on auth request with 503: " + err);
@@ -48,22 +43,22 @@ exports.process = function(req, res) {
         return res.json({ success: false });
       }
 
-      logger.info("updating password for email " + req.session.userid);
-      wsapi.bcryptPassword(req.body.newpass, function(err, hash) {
+      logger.info("updating password for user " + req.session.userid);
+      wsapi.bcryptPassword(req.params.newpass, function(err, hash) {
         if (err) {
           if (err.indexOf('exceeded') != -1) {
             logger.warn("max load hit, failing on auth request with 503: " + err);
             res.status(503);
             return res.json({ success: false, reason: "server is too busy" });
           }
-          logger.error("error bcrypting  password for password update for " + req.body.email, err);
+          logger.error("error bcrypting password for password update for user " + req.session.userid, err);
           return res.json({ success: false });
         }
 
         db.updatePassword(req.session.userid, hash, function(err) {
           var success = true;
           if (err) {
-            logger.error("error updating bcrypted password for email " + req.body.email, err);
+            logger.error("error updating bcrypted password for user " + req.session.userid, err);
             wsapi.databaseDown(res, err);
           } else {
             res.json({ success: success });
diff --git a/lib/wsapi/user_creation_status.js b/lib/wsapi/user_creation_status.js
index 41852b7278743be2cea352787e48fd2aaa471b72..9997ec01c8ba4d565d335b15c33077861d160632 100644
--- a/lib/wsapi/user_creation_status.js
+++ b/lib/wsapi/user_creation_status.js
@@ -9,11 +9,13 @@ wsapi = require('../wsapi.js');
 exports.method = 'get';
 exports.writes_db = false;
 exports.authed = false;
-exports.args = ['email'];
+exports.args = {
+  'email': 'email'
+};
 exports.i18n = false;
 
 exports.process = function(req, res) {
-  var email = req.query.email;
+  var email = req.params.email;
 
   // if the user is authenticated as the user in question, we're done
   if (wsapi.isAuthed(req, 'assertion')) {
@@ -23,7 +25,7 @@ exports.process = function(req, res) {
       else notAuthed();
     });
   } else {
-    notAuthed()
+    notAuthed();
   }
 
   function notAuthed() {
diff --git a/package.json b/package.json
index 24a3bdf1e613fdced528e5c488f7be92f699ae34..b919e5a2f6a0adb118ac84b9552e51e44987d9aa 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
         "uglifycss": "0.0.5",
         "underscore": "1.3.1",
         "urlparse": "0.0.1",
+        "validator": "0.4.9",
         "winston": "0.5.6"
     },
     "devDependencies": {
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index 99116c22ffabfc7240808018866584c1226a2b7e..1a23e87b0644c0b73c6e8140268008886d19a011 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -238,7 +238,6 @@ BrowserID.Mocks.xhr = (function() {
     }
   };
 
-
   return xhr;
 }());
 
diff --git a/tests/internal-wsapi-test.js b/tests/internal-wsapi-test.js
old mode 100644
new mode 100755
diff --git a/tests/password-length-test.js b/tests/password-length-test.js
index dc7a2b02230b9dfac5438c093f1ab6a4369537a1..9dc384501fa94893592d5b44a6bb54f6fa7ea9a4 100755
--- a/tests/password-length-test.js
+++ b/tests/password-length-test.js
@@ -44,7 +44,7 @@ suite.addBatch({
     }),
     "causes a HTTP error response": function(err, r) {
       assert.equal(r.code, 400);
-      assert.equal(r.body, "Bad Request: missing 'pass' argument");
+      assert.strictEqual(JSON.parse(r.body).success, false);
     }
   },
   "a password that is too short": {
@@ -55,7 +55,7 @@ suite.addBatch({
     }),
     "causes a HTTP error response": function(err, r) {
       assert.equal(r.code, 400);
-      assert.equal(r.body, "Bad Request: valid passwords are between 8 and 80 chars");
+      assert.equal(JSON.parse(r.body).success, false);
     }
   },
   "a password that is too long": {
@@ -66,7 +66,7 @@ suite.addBatch({
     }),
     "causes a HTTP error response": function(err, r) {
       assert.equal(r.code, 400);
-      assert.equal(r.body, "Bad Request: valid passwords are between 8 and 80 chars");
+      assert.equal(JSON.parse(r.body).success, false);
     }
   },
   "but a password that is just right": {
diff --git a/tests/registration-status-wsapi-test.js b/tests/registration-status-wsapi-test.js
index 10944b1ae2388549ddf83fad688ca8fff0996228..0a1b42a8b01aaf8deddf57248cd8f817618f3ffa 100755
--- a/tests/registration-status-wsapi-test.js
+++ b/tests/registration-status-wsapi-test.js
@@ -91,7 +91,7 @@ suite.addBatch({
       assert.strictEqual(r.code, 400);
     },
     "returns an error string": function (err, r) {
-      assert.strictEqual(r.body, "Bad Request: missing 'email' argument");
+      assert.strictEqual(JSON.parse(r.body).success, false);
     }
   }
 });
diff --git a/tests/remove-email-test.js b/tests/remove-email-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..76a3ca088d9b3ce23efe3481a0776eb2af0e60fd
--- /dev/null
+++ b/tests/remove-email-test.js
@@ -0,0 +1,181 @@
+#!/usr/bin/env node
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+require('./lib/test_env.js');
+
+const assert = require('assert'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js'),
+email = require('../lib/email.js'),
+jwcrypto = require('jwcrypto');
+
+var suite = vows.describe('forgotten-email');
+
+// algs
+require("jwcrypto/lib/algs/ds");
+require("jwcrypto/lib/algs/rs");
+
+start_stop.addStartupBatches(suite);
+
+// every time a new token is sent out, let's update the global
+// var 'token'
+var token = undefined;
+
+// create a new account via the api with (first address)
+suite.addBatch({
+  "staging an account": {
+    topic: wsapi.post('/wsapi/stage_user', {
+      email: 'first@fakeemail.com',
+      pass: 'firstfakepass',
+      site:'http://localhost:123'
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      start_stop.waitForToken(this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
+suite.addBatch({
+  "create first account": {
+    topic: function() {
+      wsapi.post('/wsapi/complete_user_creation', { token: token }).call(this);
+    },
+    "account created": function(err, r) {
+      assert.equal(r.code, 200);
+      assert.strictEqual(true, JSON.parse(r.body).success);
+      token = undefined;
+    }
+  }
+});
+
+suite.addBatch({
+  "email created": {
+    topic: wsapi.get('/wsapi/user_creation_status', { email: 'first@fakeemail.com' } ),
+    "should exist": function(err, r) {
+      assert.strictEqual(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).status, "complete");
+    }
+  }
+});
+
+// add a new email address to the account (second address)
+suite.addBatch({
+  "add a new email address to our account": {
+    topic: wsapi.post('/wsapi/stage_email', {
+      email: 'second@fakeemail.com',
+      site:'https://fakesite.foobar.bizbaz.uk'
+    }),
+    "works": function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+// wait for the token
+suite.addBatch({
+  "a token": {
+    topic: function() {
+      start_stop.waitForToken(this.callback);
+    },
+    "is obtained": function (t) {
+      assert.strictEqual(typeof t, 'string');
+      token = t;
+    }
+  }
+});
+
+// confirm second email email address to the account
+suite.addBatch({
+  "create second account": {
+    topic: function() {
+      wsapi.post('/wsapi/complete_email_confirmation', { token: token }).call(this);
+    },
+    "account created": function(err, r) {
+      assert.equal(r.code, 200);
+      assert.strictEqual(JSON.parse(r.body).success, true);
+      token = undefined;
+    }
+  }
+});
+
+// verify now both email addresses are known
+suite.addBatch({
+  "first email exists": {
+    topic: wsapi.get('/wsapi/have_email', { email: 'first@fakeemail.com' }),
+    "should exist": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).email_known, true);
+    }
+  },
+  "second email exists": {
+    topic: wsapi.get('/wsapi/have_email', { email: 'second@fakeemail.com' }),
+    "should exist": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).email_known, true);
+    }
+  },
+  "a random email doesn't exist": {
+    topic: wsapi.get('/wsapi/have_email', { email: 'third@fakeemail.com' }),
+    "shouldn't exist": function(err, r) {
+      assert.strictEqual(JSON.parse(r.body).email_known, false);
+    }
+  }
+});
+
+suite.addBatch({
+  "list emails API": {
+    topic: wsapi.get('/wsapi/list_emails', {}),
+    "succeeds with HTTP 200" : function(err, r) {
+      assert.strictEqual(r.code, 200);
+    },
+    "returns two emails": function(err, r) {
+      r = Object.keys(JSON.parse(r.body));
+      assert.ok(r.indexOf('first@fakeemail.com') != -1);
+      assert.ok(r.indexOf('second@fakeemail.com') != -1);
+    }
+  }
+});
+
+suite.addBatch({
+  "remove email": {
+    topic: wsapi.post('/wsapi/remove_email', { email: 'second@fakeemail.com'}),
+    "succeeds with HTTP 200" : function(err, r) {
+      assert.strictEqual(r.code, 200);
+    }
+  }
+});
+
+suite.addBatch({
+  "list emails API": {
+    topic: wsapi.get('/wsapi/list_emails', {}),
+    "succeeds with HTTP 200" : function(err, r) {
+      assert.strictEqual(r.code, 200);
+    },
+    "returns one emails": function(err, r) {
+      r = Object.keys(JSON.parse(r.body));
+      assert.ok(r.indexOf('first@fakeemail.com') !== -1);
+      assert.ok(r.indexOf('second@fakeemail.com') === -1);
+    }
+  }
+});
+
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js
index 5337ad2950c1cc19285fec08f8547ecc73e9a548..889dad843334ea4895a75cf3b40b63c81760827b 100755
--- a/tests/stalled-mysql-test.js
+++ b/tests/stalled-mysql-test.js
@@ -121,7 +121,7 @@ suite.addBatch({
   },
   "complete_email_confirmation": {
     topic: wsapi.post('/wsapi/complete_email_confirmation', {
-      token: 'bogus'
+      token: 'bogusbogusbogusbogusbogusbogusbogusbogusbogusbog'
     }),
     "fails with 503": function(err, r) {
       assert.strictEqual(r.code, 503);
@@ -129,7 +129,7 @@ suite.addBatch({
   },
   "complete_user_creation": {
     topic: wsapi.post('/wsapi/complete_user_creation', {
-      token: 'bogus',
+      token: 'bogusbogusbogusbogusbogusbogusbogusbogusbogusbog',
       pass: 'alsobogus'
     }),
     "fails with 503": function(err, r) {
@@ -138,7 +138,7 @@ suite.addBatch({
   },
   "email_for_token": {
     topic: wsapi.get('/wsapi/email_for_token', {
-      token: 'bogus'
+      token: 'bogusbogusbogusbogusbogusbogusbogusbogusbogusbog'
     }),
     "fails with 503": function(err, r) {
       assert.strictEqual(r.code, 503);
@@ -229,7 +229,7 @@ suite.addBatch({
   "cert_key": {
     topic: wsapi.post('/wsapi/cert_key', {
       email: "test@whatev.er",
-      pubkey: "bogus",
+      pubkey: JSON.stringify("bogusbogusbogusbogusbogusbogusbogusbogusbogusbogusbogus"),
       ephemeral: false
     }),
     "fails with 503": function(err, r) {