diff --git a/examples/client.cc b/examples/client.cc
index b482d034ecd9f6858a1a169e54cb9220abfc9efd..f2c444299d37b3381ad8f49805b1a6785f90c421 100644
--- a/examples/client.cc
+++ b/examples/client.cc
@@ -479,7 +479,7 @@ int recv_crypto_data(ngtcp2_conn *conn, ngtcp2_crypto_level crypto_level,
 } // namespace
 
 namespace {
-int recv_stream_data(ngtcp2_conn *conn, int64_t stream_id, int fin,
+int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id,
                      uint64_t offset, const uint8_t *data, size_t datalen,
                      void *user_data, void *stream_user_data) {
   if (!config.quiet && !config.no_quic_dump) {
@@ -488,7 +488,7 @@ int recv_stream_data(ngtcp2_conn *conn, int64_t stream_id, int fin,
 
   auto c = static_cast<Client *>(user_data);
 
-  if (c->recv_stream_data(stream_id, fin, data, datalen) != 0) {
+  if (c->recv_stream_data(flags, stream_id, data, datalen) != 0) {
     return NGTCP2_ERR_CALLBACK_FAILURE;
   }
 
@@ -1682,10 +1682,10 @@ int Client::submit_http_request(const Stream *stream) {
   return 0;
 }
 
-int Client::recv_stream_data(int64_t stream_id, int fin, const uint8_t *data,
-                             size_t datalen) {
-  auto nconsumed =
-      nghttp3_conn_read_stream(httpconn_, stream_id, data, datalen, fin);
+int Client::recv_stream_data(uint32_t flags, int64_t stream_id,
+                             const uint8_t *data, size_t datalen) {
+  auto nconsumed = nghttp3_conn_read_stream(
+      httpconn_, stream_id, data, datalen, flags & NGTCP2_STREAM_DATA_FLAG_FIN);
   if (nconsumed < 0) {
     std::cerr << "nghttp3_conn_read_stream: " << nghttp3_strerror(nconsumed)
               << std::endl;
diff --git a/examples/client.h b/examples/client.h
index e84434ad5fb337db0f92f7f7c643f0ca64804b47..f1b4a92e857db4a891b1b65fd07edb6cb7f56c4a 100644
--- a/examples/client.h
+++ b/examples/client.h
@@ -249,7 +249,7 @@ public:
 
   int setup_httpconn();
   int submit_http_request(const Stream *stream);
-  int recv_stream_data(int64_t stream_id, int fin, const uint8_t *data,
+  int recv_stream_data(uint32_t flags, int64_t stream_id, const uint8_t *data,
                        size_t datalen);
   int acked_stream_data_offset(int64_t stream_id, uint64_t datalen);
   int http_acked_stream_data(int64_t stream_id, size_t datalen);
diff --git a/examples/server.cc b/examples/server.cc
index 70aa4daeb4aa92905f3546df5651dc3b98a19240..97d512650a0042427e06397e3e4a60799f2c7923 100644
--- a/examples/server.cc
+++ b/examples/server.cc
@@ -882,12 +882,12 @@ int recv_crypto_data(ngtcp2_conn *conn, ngtcp2_crypto_level crypto_level,
 } // namespace
 
 namespace {
-int recv_stream_data(ngtcp2_conn *conn, int64_t stream_id, int fin,
+int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id,
                      uint64_t offset, const uint8_t *data, size_t datalen,
                      void *user_data, void *stream_user_data) {
   auto h = static_cast<Handler *>(user_data);
 
-  if (h->recv_stream_data(stream_id, fin, data, datalen) != 0) {
+  if (h->recv_stream_data(flags, stream_id, data, datalen) != 0) {
     return NGTCP2_ERR_CALLBACK_FAILURE;
   }
 
@@ -1966,7 +1966,7 @@ void Handler::schedule_retransmit() {
   ev_timer_again(loop_, &rttimer_);
 }
 
-int Handler::recv_stream_data(int64_t stream_id, uint8_t fin,
+int Handler::recv_stream_data(uint32_t flags, int64_t stream_id,
                               const uint8_t *data, size_t datalen) {
   if (!config.quiet && !config.no_quic_dump) {
     debug::print_stream_data(stream_id, data, datalen);
@@ -1976,8 +1976,8 @@ int Handler::recv_stream_data(int64_t stream_id, uint8_t fin,
     return 0;
   }
 
-  auto nconsumed =
-      nghttp3_conn_read_stream(httpconn_, stream_id, data, datalen, fin);
+  auto nconsumed = nghttp3_conn_read_stream(
+      httpconn_, stream_id, data, datalen, flags & NGTCP2_STREAM_DATA_FLAG_FIN);
   if (nconsumed < 0) {
     std::cerr << "nghttp3_conn_read_stream: " << nghttp3_strerror(nconsumed)
               << std::endl;
diff --git a/examples/server.h b/examples/server.h
index 710d34011fa7889fad6bdac738aa9370fd41bdf2..9954ca4c6bbaa1ed772baa45274b5fa141893a12 100644
--- a/examples/server.h
+++ b/examples/server.h
@@ -242,7 +242,7 @@ public:
   Server *server() const;
   const Address &remote_addr() const;
   ngtcp2_conn *conn() const;
-  int recv_stream_data(int64_t stream_id, uint8_t fin, const uint8_t *data,
+  int recv_stream_data(uint32_t flags, int64_t stream_id, const uint8_t *data,
                        size_t datalen);
   int acked_stream_data_offset(int64_t stream_id, uint64_t datalen);
   const ngtcp2_cid *scid() const;
diff --git a/lib/includes/ngtcp2/ngtcp2.h b/lib/includes/ngtcp2/ngtcp2.h
index fa077b4b2a504133a72b0033012168a69cbc50ec..c112776bdd6a29df2b8854fcfbb795e13a363b61 100644
--- a/lib/includes/ngtcp2/ngtcp2.h
+++ b/lib/includes/ngtcp2/ngtcp2.h
@@ -1261,23 +1261,46 @@ typedef int (*ngtcp2_decrypt)(uint8_t *dest, const ngtcp2_crypto_aead *aead,
 typedef int (*ngtcp2_hp_mask)(uint8_t *dest, const ngtcp2_crypto_cipher *hp,
                               const uint8_t *hp_key, const uint8_t *sample);
 
+/**
+ * @enum
+ *
+ * ngtcp2_stream_data_flag defines the properties of the data emitted
+ * via :type:`ngtcp2_recv_stream_data` callback function.
+ */
+typedef enum ngtcp2_stream_data_flag {
+  NGTCP2_STREAM_DATA_FLAG_NONE = 0x00,
+  /**
+   * NGTCP2_STREAM_DATA_FLAG_FIN indicates that this chunk of data is
+   * final piece of an incoming stream.
+   */
+  NGTCP2_STREAM_DATA_FLAG_FIN = 0x01,
+  /**
+   * NGTCP2_STREAM_DATA_FLAG_0RTT indicates that this chunk of data
+   * contains data received in 0RTT packet and the handshake has not
+   * been completed yet, which means that the data might be replayed.
+   */
+  NGTCP2_STREAM_DATA_FLAG_0RTT = 0x02
+} ngtcp2_stream_data_flag;
+
 /**
  * @functypedef
  *
  * :type:`ngtcp2_recv_stream_data` is invoked when stream data is
- * received.  The stream is specified by |stream_id|.  If |fin| is
- * nonzero, this portion of the data is the last data in this stream.
- * |offset| is the offset where this data begins.  The library ensures
- * that data is passed to the application in the non-decreasing order
- * of |offset|.  The data is passed as |data| of length |datalen|.
- * |datalen| may be 0 if and only if |fin| is nonzero.
+ * received.  The stream is specified by |stream_id|.  |flags| is the
+ * bitwise-OR of zero or more of ngtcp2_stream_data_flag.  If |flags|
+ * & :enum:`NGTCP2_STREAM_DATA_FLAG_FIN` is nonzero, this portion of
+ * the data is the last data in this stream.  |offset| is the offset
+ * where this data begins.  The library ensures that data is passed to
+ * the application in the non-decreasing order of |offset|.  The data
+ * is passed as |data| of length |datalen|.  |datalen| may be 0 if and
+ * only if |fin| is nonzero.
  *
  * The callback function must return 0 if it succeeds, or
  * :enum:`NGTCP2_ERR_CALLBACK_FAILURE` which makes the library return
  * immediately.
  */
-typedef int (*ngtcp2_recv_stream_data)(ngtcp2_conn *conn, int64_t stream_id,
-                                       int fin, uint64_t offset,
+typedef int (*ngtcp2_recv_stream_data)(ngtcp2_conn *conn, uint32_t flags,
+                                       int64_t stream_id, uint64_t offset,
                                        const uint8_t *data, size_t datalen,
                                        void *user_data, void *stream_user_data);
 
diff --git a/lib/ngtcp2_conn.c b/lib/ngtcp2_conn.c
index 3cf43d17c04872fda5fd46867d45169d0f925429..1e6da73b1f4c6cd91d5ca4635107102c5b995a33 100644
--- a/lib/ngtcp2_conn.c
+++ b/lib/ngtcp2_conn.c
@@ -81,7 +81,7 @@ static int conn_call_handshake_completed(ngtcp2_conn *conn) {
 }
 
 static int conn_call_recv_stream_data(ngtcp2_conn *conn, ngtcp2_strm *strm,
-                                      int fin, uint64_t offset,
+                                      uint32_t flags, uint64_t offset,
                                       const uint8_t *data, size_t datalen) {
   int rv;
 
@@ -89,7 +89,7 @@ static int conn_call_recv_stream_data(ngtcp2_conn *conn, ngtcp2_strm *strm,
     return 0;
   }
 
-  rv = conn->callbacks.recv_stream_data(conn, strm->stream_id, fin, offset,
+  rv = conn->callbacks.recv_stream_data(conn, flags, strm->stream_id, offset,
                                         data, datalen, conn->user_data,
                                         strm->stream_user_data);
   if (rv != 0) {
@@ -4873,6 +4873,8 @@ static int conn_emit_pending_stream_data(ngtcp2_conn *conn, ngtcp2_strm *strm,
   const uint8_t *data;
   int rv;
   uint64_t offset;
+  uint32_t sdflags;
+  int handshake_completed = conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED;
 
   for (;;) {
     /* Stop calling callback if application has called
@@ -4891,10 +4893,16 @@ static int conn_emit_pending_stream_data(ngtcp2_conn *conn, ngtcp2_strm *strm,
     offset = rx_offset;
     rx_offset += datalen;
 
-    rv = conn_call_recv_stream_data(conn, strm,
-                                    (strm->flags & NGTCP2_STRM_FLAG_SHUT_RD) &&
-                                        rx_offset == strm->rx.last_offset,
-                                    offset, data, datalen);
+    sdflags = NGTCP2_STREAM_DATA_FLAG_NONE;
+    if ((strm->flags & NGTCP2_STRM_FLAG_SHUT_RD) &&
+        rx_offset == strm->rx.last_offset) {
+      sdflags |= NGTCP2_STREAM_DATA_FLAG_FIN;
+    }
+    if (!handshake_completed) {
+      sdflags |= NGTCP2_STREAM_DATA_FLAG_0RTT;
+    }
+
+    rv = conn_call_recv_stream_data(conn, strm, sdflags, offset, data, datalen);
     if (rv != 0) {
       return rv;
     }
@@ -5054,6 +5062,7 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr,
   int local_stream;
   int bidi;
   size_t datalen = ngtcp2_vec_len(fr->data, fr->datacnt);
+  uint32_t sdflags = NGTCP2_STREAM_DATA_FLAG_NONE;
 
   local_stream = conn_local_stream(conn, fr->stream_id);
   bidi = bidi_stream(fr->stream_id);
@@ -5167,7 +5176,8 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr,
       }
 
       if (fr_end_offset == rx_offset) {
-        rv = conn_call_recv_stream_data(conn, strm, 1, rx_offset, NULL, 0);
+        rv = conn_call_recv_stream_data(conn, strm, NGTCP2_STREAM_DATA_FLAG_FIN,
+                                        rx_offset, NULL, 0);
         if (rv != 0) {
           return rv;
         }
@@ -5219,7 +5229,14 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr,
           rx_offset == strm->rx.last_offset;
 
     if (fin || datalen) {
-      rv = conn_call_recv_stream_data(conn, strm, fin, offset, data, datalen);
+      if (fin) {
+        sdflags |= NGTCP2_STREAM_DATA_FLAG_FIN;
+      }
+      if (!(conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED)) {
+        sdflags |= NGTCP2_STREAM_DATA_FLAG_0RTT;
+      }
+      rv = conn_call_recv_stream_data(conn, strm, sdflags, offset, data,
+                                      datalen);
       if (rv != 0) {
         return rv;
       }
diff --git a/tests/ngtcp2_conn_test.c b/tests/ngtcp2_conn_test.c
index dbc96bce2a72aa745ce17d74e4d3464ea1c9c28a..5ab7f6a97bda6ecc7b087cd05bc74c53040fac0a 100644
--- a/tests/ngtcp2_conn_test.c
+++ b/tests/ngtcp2_conn_test.c
@@ -134,7 +134,7 @@ typedef struct {
      recv_stream_data callback. */
   struct {
     int64_t stream_id;
-    int fin;
+    uint32_t flags;
     size_t datalen;
   } stream_data;
 } my_user_data;
@@ -281,10 +281,10 @@ static int recv_crypto_data_server(ngtcp2_conn *conn,
   return 0;
 }
 
-static int recv_stream_data(ngtcp2_conn *conn, int64_t stream_id, int fin,
-                            uint64_t offset, const uint8_t *data,
-                            size_t datalen, void *user_data,
-                            void *stream_user_data) {
+static int recv_stream_data(ngtcp2_conn *conn, uint32_t flags,
+                            int64_t stream_id, uint64_t offset,
+                            const uint8_t *data, size_t datalen,
+                            void *user_data, void *stream_user_data) {
   my_user_data *ud = user_data;
   (void)conn;
   (void)offset;
@@ -293,7 +293,7 @@ static int recv_stream_data(ngtcp2_conn *conn, int64_t stream_id, int fin,
 
   if (ud) {
     ud->stream_data.stream_id = stream_id;
-    ud->stream_data.fin = fin;
+    ud->stream_data.flags = flags;
     ud->stream_data.datalen = datalen;
   }
 
@@ -301,13 +301,13 @@ static int recv_stream_data(ngtcp2_conn *conn, int64_t stream_id, int fin,
 }
 
 static int
-recv_stream_data_shutdown_stream_read(ngtcp2_conn *conn, int64_t stream_id,
-                                      int fin, uint64_t offset,
+recv_stream_data_shutdown_stream_read(ngtcp2_conn *conn, uint32_t flags,
+                                      int64_t stream_id, uint64_t offset,
                                       const uint8_t *data, size_t datalen,
                                       void *user_data, void *stream_user_data) {
   int rv;
 
-  recv_stream_data(conn, stream_id, fin, offset, data, datalen, user_data,
+  recv_stream_data(conn, flags, stream_id, offset, data, datalen, user_data,
                    stream_user_data);
 
   rv = ngtcp2_conn_shutdown_stream_read(conn, stream_id, NGTCP2_APP_ERR01);
@@ -2789,7 +2789,8 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(4 == ud.stream_data.stream_id);
-  CU_ASSERT(0 == ud.stream_data.fin);
+  CU_ASSERT(!(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN));
+  CU_ASSERT(!(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_0RTT));
   CU_ASSERT(111 == ud.stream_data.datalen);
 
   fr.type = NGTCP2_FRAME_STREAM;
@@ -2808,7 +2809,7 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(4 == ud.stream_data.stream_id);
-  CU_ASSERT(1 == ud.stream_data.fin);
+  CU_ASSERT(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN);
   CU_ASSERT(99 == ud.stream_data.datalen);
 
   ngtcp2_conn_del(conn);
@@ -2835,7 +2836,7 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(4 == ud.stream_data.stream_id);
-  CU_ASSERT(0 == ud.stream_data.fin);
+  CU_ASSERT(!(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN));
   CU_ASSERT(111 == ud.stream_data.datalen);
 
   fr.type = NGTCP2_FRAME_STREAM;
@@ -2852,7 +2853,7 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(4 == ud.stream_data.stream_id);
-  CU_ASSERT(1 == ud.stream_data.fin);
+  CU_ASSERT(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN);
   CU_ASSERT(0 == ud.stream_data.datalen);
 
   ngtcp2_conn_del(conn);
@@ -2880,7 +2881,7 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(4 == ud.stream_data.stream_id);
-  CU_ASSERT(1 == ud.stream_data.fin);
+  CU_ASSERT(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN);
   CU_ASSERT(111 == ud.stream_data.datalen);
 
   pktlen = write_single_frame_pkt(conn, buf, sizeof(buf), &conn->oscid,
@@ -2891,7 +2892,7 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(0 == ud.stream_data.stream_id);
-  CU_ASSERT(0 == ud.stream_data.fin);
+  CU_ASSERT(!(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN));
   CU_ASSERT(0 == ud.stream_data.datalen);
 
   ngtcp2_conn_del(conn);
@@ -2933,7 +2934,7 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(4 == ud.stream_data.stream_id);
-  CU_ASSERT(1 == ud.stream_data.fin);
+  CU_ASSERT(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN);
   CU_ASSERT(599 == ud.stream_data.datalen);
 
   ngtcp2_conn_del(conn);
@@ -2976,7 +2977,7 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(4 == ud.stream_data.stream_id);
-  CU_ASSERT(1 == ud.stream_data.fin);
+  CU_ASSERT(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN);
   CU_ASSERT(599 == ud.stream_data.datalen);
 
   ngtcp2_conn_del(conn);
@@ -3002,7 +3003,7 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(3 == ud.stream_data.stream_id);
-  CU_ASSERT(0 == ud.stream_data.fin);
+  CU_ASSERT(!(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN));
   CU_ASSERT(911 == ud.stream_data.datalen);
 
   ngtcp2_conn_del(conn);
@@ -3255,7 +3256,7 @@ void test_ngtcp2_conn_recv_stream_data(void) {
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(4 == ud.stream_data.stream_id);
-  CU_ASSERT(0 == ud.stream_data.fin);
+  CU_ASSERT(!(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_FIN));
   CU_ASSERT(599 == ud.stream_data.datalen);
 
   ngtcp2_conn_del(conn);
@@ -3515,10 +3516,13 @@ void test_ngtcp2_conn_recv_early_data(void) {
   ngtcp2_strm *strm;
   ngtcp2_cid rcid;
   int rv;
+  my_user_data ud;
 
   rcid_init(&rcid);
 
   setup_early_server(&conn);
+  conn->callbacks.recv_stream_data = recv_stream_data;
+  conn->user_data = &ud;
 
   fr.type = NGTCP2_FRAME_CRYPTO;
   fr.crypto.offset = 0;
@@ -3551,9 +3555,12 @@ void test_ngtcp2_conn_recv_early_data(void) {
       conn->version, &fr, null_key, null_iv, null_hp_key, sizeof(null_key),
       sizeof(null_iv));
 
+  memset(&ud, 0, sizeof(ud));
   rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(0 == rv);
+  CU_ASSERT(4 == ud.stream_data.stream_id);
+  CU_ASSERT(ud.stream_data.flags & NGTCP2_STREAM_DATA_FLAG_0RTT);
 
   spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);