diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
 # Changelog
 
+## 0.7.0.0 — 2026-06-05
+
+### Breaking Changes
+
+- `Envelope` gained a `headers :: !(Maybe Headers)` field carrying every
+  message header the source broker delivered, in order and including
+  duplicates. Direct constructions of `Envelope` must add the field.
+  `Nothing` means the adapter does not surface headers; `Just []` means
+  it does and the message had none. The new `Headers` type alias
+  (`[(ByteString, ByteString)]`) is exported from `Shibuya.Core` and
+  `Shibuya.Core.Types`. The W3C trace headers continue to appear in
+  `traceContext` as before; they now also appear verbatim in `headers`.
+
 ## 0.6.0.0 — 2026-05-31
 
 ### Breaking Changes
diff --git a/shibuya-core.cabal b/shibuya-core.cabal
--- a/shibuya-core.cabal
+++ b/shibuya-core.cabal
@@ -1,6 +1,6 @@
-cabal-version: 3.14
+cabal-version: 3.12
 name: shibuya-core
-version: 0.6.0.0
+version: 0.7.0.0
 synopsis: Supervised queue processing framework for Haskell
 description:
   A supervised queue processing framework inspired by Broadway (Elixir).
diff --git a/src/Shibuya/Core.hs b/src/Shibuya/Core.hs
--- a/src/Shibuya/Core.hs
+++ b/src/Shibuya/Core.hs
@@ -21,6 +21,7 @@
     Cursor (..),
     Attempt (..),
     Envelope (..),
+    Headers,
 
     -- * Ack Semantics
     AckDecision (..),
@@ -87,7 +88,7 @@
 import Shibuya.Core.Error (HandlerError (..), PolicyError (..), RuntimeError (..))
 import Shibuya.Core.Ingested (Ingested (..))
 import Shibuya.Core.Lease (Lease (..))
-import Shibuya.Core.Types (Attempt (..), Cursor (..), Envelope (..), MessageId (..))
+import Shibuya.Core.Types (Attempt (..), Cursor (..), Envelope (..), Headers, MessageId (..))
 import Shibuya.Handler (Handler)
 import Shibuya.Policy (Concurrency (..), Ordering (..), validatePolicy)
 import Shibuya.Runner.Halt (ProcessorHalt (..))
diff --git a/src/Shibuya/Core/Types.hs b/src/Shibuya/Core/Types.hs
--- a/src/Shibuya/Core/Types.hs
+++ b/src/Shibuya/Core/Types.hs
@@ -14,6 +14,9 @@
     -- * Message Envelope
     Envelope (..),
 
+    -- * Message Headers
+    Headers,
+
     -- * Trace Context
     TraceHeaders,
   )
@@ -49,6 +52,16 @@
   deriving newtype (Num, Real, Enum, Integral, Bounded)
   deriving anyclass (NFData)
 
+-- | Raw message headers as delivered by the source broker.
+--
+-- An ordered list of @(key, value)@ byte-string pairs. Order is
+-- preserved and duplicate keys are allowed, because brokers such as
+-- Kafka permit multiple headers with the same key and define header
+-- order. Keys and values are raw 'ByteString' because header values
+-- are not guaranteed to be UTF-8 text (for example a binary schema
+-- id); decoding is left to the handler.
+type Headers = [(ByteString, ByteString)]
+
 -- | W3C Trace Context headers for distributed tracing.
 -- Contains traceparent and optionally tracestate headers.
 type TraceHeaders = [(ByteString, ByteString)]
@@ -66,6 +79,17 @@
     enqueuedAt :: !(Maybe UTCTime),
     -- | W3C trace context headers for distributed tracing
     traceContext :: !(Maybe TraceHeaders),
+    -- | All message headers as delivered by the source broker, in
+    -- order and including duplicates.
+    --
+    -- 'Nothing' means the adapter does not surface headers at all;
+    -- 'Just []' means the adapter surfaces headers and this message
+    -- carried none. The W3C trace headers ('traceparent' /
+    -- 'tracestate') appear here verbatim /in addition to/ their
+    -- parsed form in 'traceContext'; this field is the faithful,
+    -- non-lossy view and 'traceContext' is the narrow projection the
+    -- framework uses to re-establish a parent span.
+    headers :: !(Maybe Headers),
     -- | Optional zero-indexed delivery counter.
     -- 'Just (Attempt 0)' on first delivery; 'Nothing' if the adapter
     -- does not track redeliveries (e.g., Kafka).
@@ -105,6 +129,7 @@
         rnf e.partition `seq`
           rnf e.enqueuedAt `seq`
             rnf e.traceContext `seq`
-              rnf e.attempt `seq`
-                e.attributes `seq`
-                  rnf e.payload
+              rnf e.headers `seq`
+                rnf e.attempt `seq`
+                  e.attributes `seq`
+                    rnf e.payload
diff --git a/test/Shibuya/Core/RetrySpec.hs b/test/Shibuya/Core/RetrySpec.hs
--- a/test/Shibuya/Core/RetrySpec.hs
+++ b/test/Shibuya/Core/RetrySpec.hs
@@ -22,6 +22,7 @@
       partition = Nothing,
       enqueuedAt = Nothing,
       traceContext = Nothing,
+      headers = Nothing,
       attempt = a,
       attributes = HashMap.empty,
       payload = ()
diff --git a/test/Shibuya/Core/TypesSpec.hs b/test/Shibuya/Core/TypesSpec.hs
--- a/test/Shibuya/Core/TypesSpec.hs
+++ b/test/Shibuya/Core/TypesSpec.hs
@@ -79,6 +79,14 @@
           mapped = fmap show env
       mapped.attempt `shouldBe` Just (Attempt 3)
 
+    it "preserves headers through fmap" $ do
+      let env = (testEnvelope (1 :: Int)) {headers = Just [("content-type", "application/json")]}
+          mapped = fmap show env
+      mapped.headers `shouldBe` Just [("content-type", "application/json")]
+
+    it "defaults headers to Nothing in the test helper" $ do
+      (testEnvelope (1 :: Int)).headers `shouldBe` Nothing
+
 -- Test helper
 testEnvelope :: msg -> Envelope msg
 testEnvelope msg =
@@ -88,6 +96,7 @@
       partition = Just "partition-0",
       enqueuedAt = Just testTime,
       traceContext = Nothing,
+      headers = Nothing,
       attempt = Nothing,
       attributes = HashMap.empty,
       payload = msg
diff --git a/test/Shibuya/Runner/SupervisedSpec.hs b/test/Shibuya/Runner/SupervisedSpec.hs
--- a/test/Shibuya/Runner/SupervisedSpec.hs
+++ b/test/Shibuya/Runner/SupervisedSpec.hs
@@ -854,6 +854,7 @@
                 partition = Nothing,
                 enqueuedAt = Just testTime,
                 traceContext = Nothing,
+                headers = Nothing,
                 attempt = Nothing,
                 attributes = HashMap.empty,
                 payload = "message-" <> show i
@@ -878,6 +879,7 @@
             partition = Nothing,
             enqueuedAt = Just testTime,
             traceContext = Nothing,
+            headers = Nothing,
             attempt = Nothing,
             attributes = HashMap.empty,
             payload = "message-" <> show i
diff --git a/test/Shibuya/RunnerSpec.hs b/test/Shibuya/RunnerSpec.hs
--- a/test/Shibuya/RunnerSpec.hs
+++ b/test/Shibuya/RunnerSpec.hs
@@ -206,6 +206,7 @@
                 partition = Nothing,
                 enqueuedAt = Just testTime,
                 traceContext = Nothing,
+                headers = Nothing,
                 attempt = Nothing,
                 attributes = HashMap.empty,
                 payload = "message-" <> show i
@@ -231,6 +232,7 @@
                 partition = Nothing,
                 enqueuedAt = Just testTime,
                 traceContext = Nothing,
+                headers = Nothing,
                 attempt = Nothing,
                 attributes = HashMap.empty,
                 payload = "message-" <> show i
diff --git a/test/Shibuya/Telemetry/SemanticSpec.hs b/test/Shibuya/Telemetry/SemanticSpec.hs
--- a/test/Shibuya/Telemetry/SemanticSpec.hs
+++ b/test/Shibuya/Telemetry/SemanticSpec.hs
@@ -64,6 +64,7 @@
                 partition = Nothing,
                 enqueuedAt = Nothing,
                 traceContext = Nothing,
+                headers = Nothing,
                 attempt = Nothing,
                 attributes = HashMap.empty,
                 payload = ("hello" :: Text)
@@ -118,6 +119,7 @@
                 partition = Nothing,
                 enqueuedAt = Nothing,
                 traceContext = Nothing,
+                headers = Nothing,
                 attempt = Nothing,
                 attributes =
                   HashMap.fromList
