diff --git a/drivers/at/at.c b/drivers/at/at.c
index f74b5e370211f3e6141d9fe09ce403ca4919bc08..dfbff87cf767e83e81ae4f5cbdb5ef316415ad48 100644
--- a/drivers/at/at.c
+++ b/drivers/at/at.c
@@ -266,3 +266,52 @@ out:
     }
     return res;
 }
+
+#ifdef MODULE_AT_URC
+void at_add_urc(at_dev_t *dev, at_urc_t *urc)
+{
+    assert(urc);
+    assert(urc->code);
+    assert(strlen(urc->code) != 0);
+    assert(urc->cb);
+
+    clist_rpush(&dev->urc_list, &urc->list_node);
+}
+
+void at_remove_urc(at_dev_t *dev, at_urc_t *urc)
+{
+    clist_remove(&dev->urc_list, &urc->list_node);
+}
+
+static int _check_urc(clist_node_t *node, void *arg)
+{
+    const char *buf = arg;
+    at_urc_t *urc = container_of(node, at_urc_t, list_node);
+
+    DEBUG("Trying to match with %s\n", urc->code);
+
+    if (strncmp(buf, urc->code, strlen(urc->code)) == 0) {
+        urc->cb(urc->arg, buf);
+        return 1;
+    }
+
+    return 0;
+}
+
+void at_process_urc(at_dev_t *dev, uint32_t timeout)
+{
+    char buf[AT_BUF_SIZE];
+
+    DEBUG("Processing URC (timeout=%" PRIu32 "us)\n", timeout);
+
+    ssize_t res;
+    /* keep reading while received data are shorter than EOL */
+    while ((res = at_readline(dev, buf, sizeof(buf), true, timeout)) <
+           (ssize_t)sizeof(AT_RECV_EOL_1 AT_RECV_EOL_2) - 1) {
+        if (res < 0) {
+            return;
+        }
+    }
+    clist_foreach(&dev->urc_list, _check_urc, buf);
+}
+#endif
diff --git a/drivers/include/at.h b/drivers/include/at.h
index 7f8d02848f9d690be03e4336ec1d3b85bde8d4c8..3d1808d7f2353a4e9d08b3ea9c22e8c57629e02f 100644
--- a/drivers/include/at.h
+++ b/drivers/include/at.h
@@ -39,6 +39,7 @@
 
 #include "isrpipe.h"
 #include "periph/uart.h"
+#include "clist.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -77,12 +78,41 @@ extern "C" {
 #define AT_RECV_ERROR "ERROR"
 #endif
 
+#if defined(MODULE_AT_URC) || DOXYGEN
+#ifndef AT_BUF_SIZE
+/** Internal buffer size used to process unsolicited result code data */
+#define AT_BUF_SIZE (128)
+#endif
+
+/**
+ * @brief   Unsolicited result code callback
+ *
+ * @param[in]   arg     optional argument
+ * @param[in]   code    urc string received from the device
+ */
+typedef void (*at_urc_cb_t)(void *arg, const char *code);
+
+/**
+ * @brief   Unsolicited result code data structure
+ */
+typedef struct {
+    clist_node_t list_node; /**< node list */
+    at_urc_cb_t cb;         /**< callback */
+    const char *code;       /**< URC string which must match */
+    void *arg;              /**< optional argument */
+} at_urc_t;
+
+#endif /* MODULE_AT_URC */
+
 /**
  * @brief AT device structure
  */
 typedef struct {
     isrpipe_t isrpipe;      /**< isrpipe used for getting data from uart */
     uart_t uart;            /**< UART device where the AT device is attached */
+#ifdef MODULE_AT_URC
+    clist_node_t urc_list;  /**< list to keep track of all registered urc's */
+#endif
 } at_dev_t;
 
 /**
@@ -226,6 +256,32 @@ ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, ui
  */
 void at_drain(at_dev_t *dev);
 
+#if defined(MODULE_AT_URC) || DOXYGEN
+/**
+ * @brief   Add a callback for an unsolicited response code
+ *
+ * @param[in]   dev     device to operate on
+ * @param[in]   urc     unsolicited result code to register
+ */
+void at_add_urc(at_dev_t *dev, at_urc_t *urc);
+
+/**
+ * @brief   Remove an unsolicited response code from the list
+ *
+ * @param[in]   dev     device to operate on
+ * @param[in]   urc     unsolicited result code to remove
+ */
+void at_remove_urc(at_dev_t *dev, at_urc_t *urc);
+
+/**
+ * @brief   Process out-of-band data received from the device
+ *
+ * @param[in]   dev     device to operate on
+ * @param[in]   timeout timeout (in usec)
+ */
+void at_process_urc(at_dev_t *dev, uint32_t timeout);
+#endif
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk
index 0d04d3cec7c6e1f84dd6ae2aa2a37df5051c9e69..1b4ca0337e2da2da8a556793bbfa4535b008ef00 100644
--- a/makefiles/pseudomodules.inc.mk
+++ b/makefiles/pseudomodules.inc.mk
@@ -1,3 +1,4 @@
+PSEUDOMODULES += at_urc
 PSEUDOMODULES += auto_init_gnrc_rpl
 PSEUDOMODULES += can_mbox
 PSEUDOMODULES += can_pm
diff --git a/tests/driver_at/Makefile b/tests/driver_at/Makefile
index 43ed6a582c3816f37beb3b3c10260436b0e85c79..f68ba0642896a78dc0ff4282fa92c50d7b7d4fb1 100644
--- a/tests/driver_at/Makefile
+++ b/tests/driver_at/Makefile
@@ -4,5 +4,6 @@ BOARD_INSUFFICIENT_MEMORY += nucleo-f031k6
 
 USEMODULE += shell
 USEMODULE += at
+USEMODULE += at_urc
 
 include $(RIOTBASE)/Makefile.include
diff --git a/tests/driver_at/main.c b/tests/driver_at/main.c
index 026d966d1b85375f0b90465b421a965d52668f76..7353bb5bec5a29ed86f0396e3bc50a5ed3358933 100644
--- a/tests/driver_at/main.c
+++ b/tests/driver_at/main.c
@@ -22,6 +22,7 @@
 
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 
 #include "at.h"
 #include "shell.h"
@@ -114,12 +115,101 @@ static int drain(int argc, char **argv)
     return 0;
 }
 
+#ifdef MODULE_AT_URC
+#ifndef MAX_URC_NB
+#define MAX_URC_NB  5
+#endif
+
+#ifndef MAX_URC_LEN
+#define MAX_URC_LEN 32
+#endif
+
+static at_urc_t urc_list[MAX_URC_NB];
+static char urc_str[MAX_URC_NB][MAX_URC_LEN];
+static bool urc_used[MAX_URC_NB];
+
+static void _urc_cb(void *arg, const char *urc)
+{
+    (void)arg;
+    printf("urc received: %s\n", urc);
+}
+
+static int add_urc(int argc, char **argv)
+{
+    if (argc < 2) {
+        printf("Usage: %s <urc>\n", argv[0]);
+        return 1;
+    }
+
+    if (strlen(argv[1]) > MAX_URC_LEN - 1) {
+        puts("urc is too long");
+        return 1;
+    }
+
+    for (size_t i = 0; i < MAX_URC_NB; i++) {
+        if (!urc_used[i]) {
+            strcpy(urc_str[i], argv[1]);
+            urc_list[i].code = urc_str[i];
+            urc_list[i].arg = NULL;
+            urc_list[i].cb = _urc_cb;
+            urc_used[i] = true;
+            at_add_urc(&at_dev, &urc_list[i]);
+            puts("urc registered");
+            return 0;
+        }
+    }
+
+    puts("Not enough memory, urc is not registered");
+    return 1;
+}
+
+static int process_urc(int argc, char **argv)
+{
+    if (argc < 2) {
+        printf("Usage: %s <timeout>\n", argv[0]);
+        return 1;
+    }
+
+    uint32_t timeout = strtoul(argv[1], NULL, 0);
+    at_process_urc(&at_dev, timeout);
+
+    puts("urc processed");
+
+    return 0;
+}
+
+static int remove_urc(int argc, char **argv)
+{
+    if (argc < 2) {
+        printf("Usage: %s <urc>\n", argv[0]);
+        return 1;
+    }
+
+    for (size_t i = 0; i < MAX_URC_NB; i++) {
+        if (urc_used[i] && strcmp(urc_list[i].code, argv[1]) == 0) {
+            at_remove_urc(&at_dev, &urc_list[i]);
+            urc_used[i] = false;
+            puts("urc removed");
+            return 0;
+        }
+    }
+
+    puts("urc not found");
+    return 1;
+}
+#endif
+
 static const shell_command_t shell_commands[] = {
     { "init", "Initialize AT device", init },
     { "send", "Send a command and wait response", send },
     { "send_ok", "Send a command and wait OK", send_ok },
     { "send_lines", "Send a command and wait lines", send_lines },
     { "drain", "Drain AT device", drain },
+#ifdef MODULE_AT_URC
+    { "add_urc", "Register an URC", add_urc },
+    { "remove_urc", "De-register an URC", remove_urc },
+    { "process_urc", "Process the URCs", process_urc },
+#endif
     { NULL, NULL, NULL },
 };