diff --git a/changelogs/unreleased/box_read_view.md b/changelogs/unreleased/box_read_view.md
new file mode 100644
index 0000000000000000000000000000000000000000..f487d7c906a68a999461465ff20a9701ce9a52d1
--- /dev/null
+++ b/changelogs/unreleased/box_read_view.md
@@ -0,0 +1,4 @@
+## feature/box
+
+Introduce box_read_view_* ffi API for opening/closing read views and iterating
+over index read views.
diff --git a/extra/exports b/extra/exports
index 61886c79c84db74f28847e3e1f901b0f82802956..c164164f285455f86e48d5aeba0fc203db4be173 100644
--- a/extra/exports
+++ b/extra/exports
@@ -95,6 +95,11 @@ box_latch_unlock
 box_on_shutdown
 box_read_ffi_disable
 box_read_ffi_is_disabled
+box_read_view_close
+box_read_view_iterator_all
+box_read_view_iterator_free
+box_read_view_iterator_next_raw
+box_read_view_open_for_given_spaces
 box_region_aligned_alloc
 box_region_alloc
 box_region_truncate
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 7c9e19406f482e6faf16657ee1bf80cc942f075f..22e4c112145c202475018af2eb24a30cae80a988 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -240,6 +240,7 @@ set(api_headers
     ${PROJECT_SOURCE_DIR}/src/lib/core/port.h
     ${PROJECT_SOURCE_DIR}/src/box/port.h
     ${PROJECT_SOURCE_DIR}/src/box/module_cache.h
+    ${PROJECT_SOURCE_DIR}/src/box/read_view.h
     ${EXTRA_API_HEADERS}
 )
 rebuild_module_api(${api_headers})
diff --git a/src/box/read_view.c b/src/box/read_view.c
index f53a14cd2e30a52f93e6f7a7a9f26f262589fb62..cf9ee343496adaa3810367af27e8ddb7e800ecbf 100644
--- a/src/box/read_view.c
+++ b/src/box/read_view.c
@@ -279,3 +279,133 @@ read_view_foreach(read_view_foreach_f cb, void *arg)
 	}
 	return true;
 }
+
+/**
+ * See read_view_opts::filter_arg.
+ */
+struct read_view_user_filer_arg {
+	/* See box_read_view_open_for_given_spaces. */
+	struct space_index_id *space_index_ids;
+	/* See box_read_view_open_for_given_spaces. */
+	uint32_t space_index_ids_count;
+};
+
+/**
+ * See read_view_opts::filter_space.
+ */
+static bool
+read_view_user_space_filter(struct space *space, void *a)
+{
+	struct read_view_user_filer_arg *arg = a;
+	for (uint32_t i = 0; i < arg->space_index_ids_count; i++)
+		if (space_id(space) == arg->space_index_ids[i].space_id)
+			return true;
+	return false;
+}
+
+/**
+ * See read_view_opts::filter_index.
+ */
+static bool
+read_view_user_index_filter(struct space *space, struct index *index, void *a)
+{
+	struct read_view_user_filer_arg *arg = a;
+	for (uint32_t i = 0; i < arg->space_index_ids_count; i++) {
+		if (space_id(space) != arg->space_index_ids[i].space_id)
+			continue;
+		if (index->def->iid == arg->space_index_ids[i].index_id)
+			return true;
+	}
+	return false;
+}
+
+API_EXPORT box_read_view_t *
+box_read_view_open_for_given_spaces(const char *name,
+				    struct space_index_id *space_index_ids,
+				    uint32_t space_index_ids_count,
+				    uint64_t flags)
+{
+	struct read_view_opts rv_opts;
+	read_view_opts_create(&rv_opts);
+	rv_opts.name = name;
+
+	/*
+	 * The user chooses spaces explicitly by ids,
+	 * so they may choose to add data-temporary spaces.
+	 */
+	rv_opts.enable_data_temporary_spaces = true;
+
+	if (flags & BOX_READ_VIEW_FIELD_NAMES)
+		rv_opts.enable_field_names = true;
+
+	struct read_view_user_filer_arg arg;
+	arg.space_index_ids = space_index_ids;
+	arg.space_index_ids_count = space_index_ids_count;
+	rv_opts.filter_arg = &arg;
+	rv_opts.filter_space = read_view_user_space_filter;
+	rv_opts.filter_index = read_view_user_index_filter;
+
+	box_read_view_t *rv = xcalloc(1, sizeof(box_read_view_t));
+	if (read_view_open(rv, &rv_opts) != 0) {
+		free(rv);
+		return NULL;
+	}
+
+	return rv;
+}
+
+API_EXPORT void
+box_read_view_close(box_read_view_t *rv)
+{
+	read_view_close(rv);
+	free(rv);
+}
+
+API_EXPORT int
+box_read_view_iterator_all(box_read_view_t *rv,
+			   uint32_t space_id, uint32_t index_id,
+			   box_read_view_iterator_t **iter)
+{
+	struct space_read_view *space_rv;
+	read_view_foreach_space(space_rv, rv) {
+		if (space_rv->id != space_id)
+			continue;
+
+		struct index_read_view *index_rv =
+			space_read_view_index(space_rv, index_id);
+		if (index_rv == NULL) {
+			/* Index is not in the read view. */
+			*iter = NULL;
+			return 0;
+		}
+
+		box_read_view_iterator_t *it =
+			xcalloc(1, sizeof(box_read_view_iterator_t));
+		if (index_read_view_create_iterator(index_rv, ITER_ALL,
+						    NULL, 0, it) != 0) {
+			free(it);
+			return -1;
+		}
+
+		*iter = it;
+		return 0;
+	}
+
+	/* Space is not in the read view. */
+	*iter = NULL;
+	return 0;
+}
+
+API_EXPORT int
+box_read_view_iterator_next_raw(box_read_view_iterator_t *iterator,
+				const char **data, uint32_t *size)
+{
+	return index_read_view_iterator_next_raw(iterator, data, size);
+}
+
+API_EXPORT void
+box_read_view_iterator_free(box_read_view_iterator_t *iterator)
+{
+	index_read_view_iterator_destroy(iterator);
+	free(iterator);
+}
diff --git a/src/box/read_view.h b/src/box/read_view.h
index 1af5d5a94f0fef4dfc5f4fd5e6c21676e232579e..5d609e9533f30dae4f40084239ab65e0989c7d45 100644
--- a/src/box/read_view.h
+++ b/src/box/read_view.h
@@ -214,6 +214,105 @@ read_view_foreach_f(struct read_view *rv, void *arg);
 bool
 read_view_foreach(read_view_foreach_f cb, void *arg);
 
+/** \cond public */
+
+typedef struct index_read_view_iterator box_read_view_iterator_t;
+typedef struct read_view box_read_view_t;
+
+/**
+ * Flags supported by \link box_read_view_open_for_given_spaces \endlink.
+ */
+enum box_read_view_flags {
+	BOX_READ_VIEW_FIELD_NAMES = 0x0001,
+};
+
+struct space_index_id {
+	uint32_t space_id;
+	uint32_t index_id;
+};
+
+/**
+ * Open a read view on the spaces and indexes specified by the given parameters.
+ *
+ * \param name                   Read view name. Will be copied
+ *                               so memory may be reused immediately.
+ *
+ * \param space_index_ids        Array of pairs (space id, index id) which
+ *                               should be added to the read view.
+ * \param space_index_ids_count  Number of elements in \a space_index_ids
+ *
+ * \param flags                  Read view flags. Must be a disjunction of
+ *                               enum \link box_read_view_flags \endlink values
+ *                               or 0.
+ *
+ * \retval NULL                  On error (check box_error_last()).
+ * \retval read view		 Otherwise.
+ *
+ * \sa box_read_view_iterator()
+ * \sa box_read_view_close()
+ */
+box_read_view_t *
+box_read_view_open_for_given_spaces(const char *name,
+				    struct space_index_id *space_index_ids,
+				    uint32_t space_index_ids_count,
+				    uint64_t flags);
+
+/**
+ * Close the read view and dispose off any resources taken up by it.
+ *
+ * \sa box_read_view_open_for_given_spaces()
+ */
+void
+box_read_view_close(box_read_view_t *rv);
+
+/**
+ * Create an iterator over all the tuples in the read view of the given index.
+ *
+ * \param rv         Read view returned by box_read_view_open_for_given_spaces()
+ * \param space_id   Space identifier
+ * \param index_id   Index identifier
+ * \param[out] iter  Iterator or NULL if the given index is not in the read view
+ *
+ * \retval -1        On error (check box_error_last())
+ * \retval 0         Otherwise. Index not existing is not an error
+ *
+ * \sa box_read_view_iterator_next()
+ * \sa box_read_view_iterator_free()
+ */
+int
+box_read_view_iterator_all(box_read_view_t *rv,
+			   uint32_t space_id, uint32_t index_id,
+			   box_read_view_iterator_t **iter);
+
+/**
+ * Retrieve the next item from the \a iterator.
+ *
+ * \param iterator      An iterator returned by box_read_view_iterator()
+ * \param[out] data     A pointer to the raw tuple data
+ *			or NULL if there's no more data
+ * \param[out] size     Size of the returned tuple data
+ *
+ * \retval -1           On error (check box_error_last() for details)
+ * \retval 0            On success. The end of data is not an error
+ *
+ * \sa box_read_view_iterator()
+ * \sa box_read_view_iterator_free()
+ */
+int
+box_read_view_iterator_next_raw(box_read_view_iterator_t *iterator,
+				const char **data, uint32_t *size);
+
+/**
+ * Destroy and deallocate the read view iterator.
+ *
+ * \sa box_read_view_iterator()
+ * \sa box_read_view_iterator_free()
+ */
+void
+box_read_view_iterator_free(box_read_view_iterator_t *iterator);
+
+/** \endcond public */
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/test/app-tap/module_api.c b/test/app-tap/module_api.c
index f775f60bca323ca15196512e4f5ee3ae11dba41e..b4e55c2c915a0871fdbe33393d48591a401ae1ab 100644
--- a/test/app-tap/module_api.c
+++ b/test/app-tap/module_api.c
@@ -3382,6 +3382,246 @@ test_box_auth_data_prepare(struct lua_State *L)
 	return 1;
 }
 
+#define CHECK_STREQ(expected, data, len) do {					\
+	fail_unless(strlen(expected) == (len));					\
+	fail_unless(memcmp(expected, data, len) == 0);				\
+} while (0)
+
+static int
+test_box_read_view(struct lua_State *L)
+{
+	int rc;
+	const char *data;
+	uint32_t size;
+	box_tuple_t *t;
+
+	/*
+	 * Preparation.
+	 */
+	const char code[] =
+		"local s1 = box.schema.space.create('test_box_read_view_s1')\n"
+		"s1:create_index('pk')\n"
+		"s1:put{16}\n"
+		"s1:put{32}\n"
+		"s1:put{48}\n"
+		"\n"
+		"local s2 = box.schema.space.create('test_box_read_view_s2')\n"
+		"s2:format({{'id', 'unsigned'}, {'val', 'string'}})\n"
+		"s2:create_index('pk')\n"
+		"s2:create_index('val', {parts = {'val'}})\n"
+		"s2:put{1, 'c'}\n"
+		"s2:put{2, 'b'}\n"
+		"s2:put{3, 'a'}\n"
+		"\n"
+		"local s3 = box.schema.space.create('test_box_read_view_s3')\n"
+		"s3:create_index('pk')\n"
+		"s3:put{1}\n"
+		"s3:put{2}\n"
+		"s3:put{3}\n"
+		"\n"
+		"local s4 = box.schema.space.create("
+		"    'test_box_read_view_s4', { engine = 'vinyl' }\n"
+		")\n"
+		"s4:create_index('pk')\n"
+		"s4:put{16}\n"
+		"s4:put{32}\n"
+		"s4:put{48}\n"
+		"\n"
+		"return s1.id, s2.id, s3.id, s4.id\n";
+	rc = luaL_loadbuffer(L, code, sizeof(code) - 1, __func__);
+	fail_unless(rc == 0);
+	rc = lua_pcall(L, 0, 4, 0);
+	fail_unless(rc == 0);
+	uint32_t s1_id = lua_tointeger(L, -4);
+	uint32_t s2_id = lua_tointeger(L, -3);
+	uint32_t s3_id = lua_tointeger(L, -2);
+	uint32_t s4_id = lua_tointeger(L, -1);
+
+	/* Space doesn't exist */
+	uint32_t sX_id = 69420;
+	{
+		char key[64];
+		char *key_end = key;
+		key_end = mp_encode_array(key_end, 1);
+		key_end = mp_encode_uint(key_end, sX_id);
+		box_index_get(BOX_SPACE_ID, 0, key, key_end, &t);
+		fail_unless(t == NULL);
+	}
+
+	/*
+	 * Open read view.
+	 */
+	struct space_index_id space_index_ids[] = {
+		{s1_id, 0},
+		/* Unknown index is ignored. */
+		{s3_id, 1337},
+		/* Unknown space is ignored. */
+		{sX_id, 0},
+		{s4_id, 0},
+		{s2_id, 1},
+	};
+	box_read_view_t *rv;
+	rv = box_read_view_open_for_given_spaces(__func__,
+						 space_index_ids,
+						 lengthof(space_index_ids), 0);
+	fail_unless(rv != NULL);
+
+	/*
+	 * Space index is in the read view.
+	 */
+	box_read_view_iterator_t *it;
+	rc = box_read_view_iterator_all(rv, s1_id, 0, &it);
+	fail_unless(rc == 0);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(rc == 0);
+	CHECK_STREQ("\x91\x10", data, size);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(rc == 0);
+	CHECK_STREQ("\x91\x20", data, size);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(rc == 0);
+	CHECK_STREQ("\x91\x30", data, size);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(data == NULL);
+
+	box_read_view_iterator_free(it);
+
+	/*
+	 * Space is in the read view, but index doesn't.
+	 */
+	rc = box_read_view_iterator_all(rv, s1_id, 1, &it);
+	fail_unless(rc == 0);
+	fail_unless(it == NULL);
+
+	rc = box_read_view_iterator_all(rv, s2_id, 0, &it);
+	fail_unless(rc == 0);
+	fail_unless(it == NULL);
+
+	/*
+	 * Space index is in the read view. Non-primary index.
+	 */
+	rc = box_read_view_iterator_all(rv, s2_id, 1, &it);
+	fail_unless(rc == 0);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(rc == 0);
+	CHECK_STREQ("\x92\x03\xa1\x61", data, size);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(rc == 0);
+	CHECK_STREQ("\x92\x02\xa1\x62", data, size);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(rc == 0);
+	CHECK_STREQ("\x92\x01\xa1\x63", data, size);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(data == NULL);
+
+	box_read_view_iterator_free(it);
+
+	/*
+	 * Space is not in the read view.
+	 */
+	rc = box_read_view_iterator_all(rv, s3_id, 0, &it);
+	fail_unless(rc == 0);
+	fail_unless(it == NULL);
+
+	rc = box_read_view_iterator_all(rv, s3_id, 1337, &it);
+	fail_unless(rc == 0);
+	fail_unless(it == NULL);
+
+	rc = box_read_view_iterator_all(rv, sX_id, 0, &it);
+	fail_unless(rc == 0);
+	fail_unless(it == NULL);
+
+	/*
+	 * Space is modified but read view is not.
+	 */
+	{
+		static const char key[] = "\x91\x20";
+		box_delete(s1_id, 0, key, key + sizeof(key) - 1, &t);
+		fail_unless(t != NULL);
+	}
+	{
+		static const char key[] = "\x91\x40";
+		box_insert(s1_id, key, key + sizeof(key) - 1, &t);
+		fail_unless(t != NULL);
+	}
+	{
+		static const char key[] = "\x92\x30\x45";
+		box_replace(s1_id, key, key + sizeof(key) - 1, &t);
+		fail_unless(t != NULL);
+	}
+	{
+		box_iterator_t *it;
+		ssize_t len;
+		char buf[64];
+		buf[0] = 0x90;
+		it = box_index_iterator(s1_id, 0, ITER_ALL, buf, buf + 1);
+
+		box_iterator_next(it, &t);
+		len = box_tuple_to_buf(t, buf, sizeof(buf));
+		CHECK_STREQ("\x91\x10", buf, len);
+
+		box_iterator_next(it, &t);
+		len = box_tuple_to_buf(t, buf, sizeof(buf));
+		CHECK_STREQ("\x92\x30\x45", buf, len);
+
+		box_iterator_next(it, &t);
+		len = box_tuple_to_buf(t, buf, sizeof(buf));
+		CHECK_STREQ("\x91\x40", buf, len);
+
+		box_iterator_next(it, &t);
+		fail_unless(t == NULL);
+
+		box_iterator_free(it);
+	}
+
+	/*
+	 * Read view is not modified.
+	 */
+	rc = box_read_view_iterator_all(rv, s1_id, 0, &it);
+	fail_unless(rc == 0);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(rc == 0);
+	CHECK_STREQ("\x91\x10", data, size);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(rc == 0);
+	CHECK_STREQ("\x91\x20", data, size);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(rc == 0);
+	CHECK_STREQ("\x91\x30", data, size);
+
+	rc = box_read_view_iterator_next_raw(it, &data, &size);
+	fail_unless(data == NULL);
+
+	box_read_view_iterator_free(it);
+
+	/*
+	 * Vinyl spaces don't support read views.
+	 */
+
+	rc = box_read_view_iterator_all(rv, s4_id, 0, &it);
+	fail_unless(rc == 0);
+	fail_unless(it == NULL);
+
+	/*
+	 * Close read view.
+	 */
+	box_read_view_close(rv);
+
+	lua_pushboolean(L, true);
+	return 1;
+}
+
 LUA_API int
 luaopen_module_api(lua_State *L)
 {
@@ -3444,6 +3684,7 @@ luaopen_module_api(lua_State *L)
 		{"box_iproto_send", test_box_iproto_send},
 		{"box_iproto_override_set", test_box_iproto_override_set},
 		{"box_iproto_override_reset", test_box_iproto_override_reset},
+		{"test_box_read_view", test_box_read_view},
 		{NULL, NULL}
 	};
 	luaL_register(L, "module_api", lib);
diff --git a/test/app-tap/module_api.test.lua b/test/app-tap/module_api.test.lua
index 8dcf33c5bb6112cd082cc6b4fcf9459ad41e1b2d..1788357c5b86e62ecd8b344f1b5c8ef4f2f26016 100755
--- a/test/app-tap/module_api.test.lua
+++ b/test/app-tap/module_api.test.lua
@@ -664,7 +664,7 @@ local function test_box_iproto_override(test, module)
 end
 
 require('tap').test("module_api", function(test)
-    test:plan(55)
+    test:plan(56)
     local status, module = pcall(require, 'module_api')
     test:is(status, true, "module")
     test:ok(status, "module is loaded")