diff --git a/hmp-commands.hx b/hmp-commands.hx
index bd35a3e..376dc4d 100644
--- a/hmp-commands.hx
+++ b/hmp-commands.hx
@@ -606,8 +606,7 @@ ETEXI
         .args_type  = "device:O",
         .params     = "driver[,prop=value][,...]",
         .help       = "add device, like -device on the command line",
-        .user_print = monitor_user_noop,
-        .mhandler.cmd_new = do_device_add,
+        .mhandler.cmd = hmp_device_add,
     },
 
 STEXI
diff --git a/hmp.c b/hmp.c
index 9cf2d13..23c06ec 100644
--- a/hmp.c
+++ b/hmp.c
@@ -934,3 +934,27 @@ void hmp_migrate(Monitor *mon, const QDict *qdict)
         qemu_mod_timer(status->timer, qemu_get_clock_ms(rt_clock));
     }
 }
+
+void hmp_device_add(Monitor *mon, const QDict *qdict)
+{
+    KeyValuesList *opts_list = NULL;
+    const QDictEntry *entry;
+    Error *err = NULL;
+
+    for (entry = qdict_first(qdict); entry; entry = qdict_next(qdict, entry)) {
+        const char *key = qdict_entry_key(entry);
+        const char *value = qstring_get_str(qobject_to_qstring(qdict_entry_value(entry)));
+        KeyValuesList *kv;
+
+        kv = g_malloc0(sizeof(*kv));
+        kv->value = g_malloc0(sizeof(*kv->value));
+        kv->value->key = g_strdup(key);
+        kv->value->value = g_strdup(value);
+        kv->next = opts_list;
+        opts_list = kv;
+    }
+
+    qmp_device_add(opts_list, &err);
+    qapi_free_KeyValuesList(opts_list);
+    hmp_handle_error(mon, &err);
+}
diff --git a/hmp.h b/hmp.h
index 8807853..3e75d4c 100644
--- a/hmp.h
+++ b/hmp.h
@@ -60,5 +60,6 @@ void hmp_block_stream(Monitor *mon, const QDict *qdict);
 void hmp_block_job_set_speed(Monitor *mon, const QDict *qdict);
 void hmp_block_job_cancel(Monitor *mon, const QDict *qdict);
 void hmp_migrate(Monitor *mon, const QDict *qdict);
+void hmp_device_add(Monitor *mon, const QDict *qdict);
 
 #endif
diff --git a/hw/qdev-monitor.c b/hw/qdev-monitor.c
index a310cc7..0661f55 100644
--- a/hw/qdev-monitor.c
+++ b/hw/qdev-monitor.c
@@ -19,6 +19,7 @@
 
 #include "qdev.h"
 #include "monitor.h"
+#include "qmp-commands.h"
 
 /*
  * Aliases were a bad idea from the start.  Let's keep them
@@ -388,7 +389,7 @@ static BusState *qbus_find(const char *path)
     }
 }
 
-DeviceState *qdev_device_add(QemuOpts *opts)
+DeviceState *qdev_device_add(QemuOpts *opts, Error **errp)
 {
     ObjectClass *obj;
     DeviceClass *k;
@@ -398,7 +399,7 @@ DeviceState *qdev_device_add(QemuOpts *opts)
 
     driver = qemu_opt_get(opts, "driver");
     if (!driver) {
-        qerror_report(QERR_MISSING_PARAMETER, "driver");
+        error_set(errp, QERR_MISSING_PARAMETER, "driver");
         return NULL;
     }
 
@@ -414,7 +415,7 @@ DeviceState *qdev_device_add(QemuOpts *opts)
     }
 
     if (!obj) {
-        qerror_report(QERR_INVALID_PARAMETER_VALUE, "driver", "device type");
+        error_set(errp, QERR_INVALID_PARAMETER_VALUE, "driver", "device type");
         return NULL;
     }
 
@@ -428,20 +429,18 @@ DeviceState *qdev_device_add(QemuOpts *opts)
             return NULL;
         }
         if (bus->info != k->bus_info) {
-            qerror_report(QERR_BAD_BUS_FOR_DEVICE,
-                           driver, bus->info->name);
+            error_set(errp, QERR_BAD_BUS_FOR_DEVICE, driver, bus->info->name);
             return NULL;
         }
     } else {
         bus = qbus_find_recursive(sysbus_get_default(), NULL, k->bus_info);
         if (!bus) {
-            qerror_report(QERR_NO_BUS_FOR_DEVICE,
-                          driver, k->bus_info->name);
+            error_set(errp, QERR_NO_BUS_FOR_DEVICE, driver, k->bus_info->name);
             return NULL;
         }
     }
     if (qdev_hotplug && !bus->allow_hotplug) {
-        qerror_report(QERR_BUS_NO_HOTPLUG, bus->name);
+        error_set(errp, QERR_BUS_NO_HOTPLUG, bus->name);
         return NULL;
     }
 
@@ -463,7 +462,7 @@ DeviceState *qdev_device_add(QemuOpts *opts)
         return NULL;
     }
     if (qdev_init(qdev) < 0) {
-        qerror_report(QERR_DEVICE_INIT_FAILED, driver);
+        error_set(errp, QERR_DEVICE_INIT_FAILED, driver);
         return NULL;
     }
     if (qdev->id) {
@@ -555,23 +554,58 @@ void do_info_qdm(Monitor *mon)
     object_class_foreach(qdev_print_devinfo, TYPE_DEVICE, false, NULL);
 }
 
-int do_device_add(Monitor *mon, const QDict *qdict, QObject **ret_data)
+void qmp_device_add(KeyValuesList *opts, Error **errp)
 {
-    QemuOpts *opts;
+    Error *local_err = NULL;
+    QemuOptsList *opts_list;
+    QemuOpts *qopts = NULL;
+    const char *id = NULL;
+    KeyValuesList *kv;
+
+    for (kv = opts; kv; kv = kv->next) {
+        if (!strcmp(kv->value->key, "id")) {
+            id = kv->value->key;
+            break;
+        }
+    }
 
-    opts = qemu_opts_from_qdict(qemu_find_opts("device"), qdict);
-    if (!opts) {
-        return -1;
+    opts_list = qemu_find_opts_err("device", &local_err);
+    if (error_is_set(&local_err)) {
+        goto exit_err;
+    }
+
+    qopts = qemu_opts_create(opts_list, id, 1, &local_err);
+    if (local_err) {
+        goto exit_err;
+    }
+
+    for (kv = opts; kv; kv = kv->next) {
+        qemu_opt_set_err(qopts, kv->value->key, kv->value->value, &local_err);
+        if (error_is_set(&local_err)) {
+            goto exit_err;
+        }
     }
+
+#if 0
+    FIXME: move the help to the HMP counterpart
     if (!monitor_cur_is_qmp() && qdev_device_help(opts)) {
         qemu_opts_del(opts);
         return 0;
     }
-    if (!qdev_device_add(opts)) {
-        qemu_opts_del(opts);
-        return -1;
+#endif
+
+    qdev_device_add(qopts, &local_err);
+    if (error_is_set(&local_err)) {
+        goto exit_err;
     }
-    return 0;
+
+    return;
+
+exit_err:
+    if (qopts) {
+        qemu_opts_del(qopts);
+    }
+    error_propagate(errp, local_err);
 }
 
 int do_device_del(Monitor *mon, const QDict *qdict, QObject **ret_data)
diff --git a/hw/qdev.h b/hw/qdev.h
index 9cc3f98..3b938e2 100644
--- a/hw/qdev.h
+++ b/hw/qdev.h
@@ -143,7 +143,7 @@ DeviceState *qdev_create(BusState *bus, const char *name);
 DeviceState *qdev_try_create(BusState *bus, const char *name);
 bool qdev_exists(const char *name);
 int qdev_device_help(QemuOpts *opts);
-DeviceState *qdev_device_add(QemuOpts *opts);
+DeviceState *qdev_device_add(QemuOpts *opts, Error **errp);
 int qdev_init(DeviceState *dev) QEMU_WARN_UNUSED_RESULT;
 void qdev_init_nofail(DeviceState *dev);
 void qdev_set_legacy_instance_id(DeviceState *dev, int alias_id,
diff --git a/qapi-schema.json b/qapi-schema.json
index 25bd487..68e2515 100644
--- a/qapi-schema.json
+++ b/qapi-schema.json
@@ -1716,3 +1716,26 @@
 # Since: 0.14.0
 ##
 { 'type': 'KeyValues', 'data': {'key': 'str', 'value': 'str'} }
+
+##
+# @device_add:
+#
+# Add a new device to the guest
+#
+# @device-optoins: a list of KeyValues of options specific to each device.
+#                  There are two driver indepedent options: 'driver' (which is
+#                  actually _required_) and 'id'.
+#
+# Returns: Nothing on success
+#          If @driver is not a valid device type, DeviceNotFound
+#          If @opts contains an invalid parameter for this device,
+#            InvalidParameter
+#
+# Notes: The semantics of @opts is not well defined.  Future commands will be
+#        introduced that provide stronger typing for device creation.
+#        This command performs hotplug of devices for guests.  When the command
+#        returns, the device may not be visible to the guest.
+#
+# Since: 0.14.0
+##
+{ 'command': 'device_add', 'data': {'device-options': '**' } }
diff --git a/qmp-commands.hx b/qmp-commands.hx
index c626ba8..829a043 100644
--- a/qmp-commands.hx
+++ b/qmp-commands.hx
@@ -279,8 +279,7 @@ EQMP
         .args_type  = "device:O",
         .params     = "driver[,prop=value][,...]",
         .help       = "add device, like -device on the command line",
-        .user_print = monitor_user_noop,
-        .mhandler.cmd_new = do_device_add,
+        .mhandler.cmd_new = qmp_marshal_input_device_add,
     },
 
 SQMP
diff --git a/vl.c b/vl.c
index 6467d6b..976126f 100644
--- a/vl.c
+++ b/vl.c
@@ -1821,7 +1821,8 @@ static int device_init_func(QemuOpts *opts, void *opaque)
 {
     DeviceState *dev;
 
-    dev = qdev_device_add(opts);
+    /* FIXME: */
+    dev = qdev_device_add(opts, NULL);
     if (!dev)
         return -1;
     return 0;
