diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,22 @@
 
 ## [Unreleased]
 
+## 0.9.0.0 — 2026-08-02
+
+### Breaking Changes
+
+- Requires `keiki >=0.8 && <0.9`, replacing the previous `>=0.7 && <0.8` bound.
+  Consumers must solve for the same Keiki major; verification results and
+  rendering may differ from the 0.7 line.
+
+### New Features
+
+- Adds `parseKindIdV7Text` and `parseKindIdV7Value` to
+  `Keiro.Codec.IdDomain`. Both construct a prefix-indexed `KindID` only after
+  the frozen TypeID-v7 policy succeeds; the Aeson entry point preserves the
+  owning JSON field path for malformed, wrong-prefix, non-canonical, and non-v7
+  failures.
+
 ## 0.8.0.0 — 2026-08-01
 
 No changes this release. Version moves with the package set.
diff --git a/keiro-core.cabal b/keiro-core.cabal
--- a/keiro-core.cabal
+++ b/keiro-core.cabal
@@ -1,6 +1,6 @@
 cabal-version:   3.0
 name:            keiro-core
-version:         0.8.0.0
+version:         0.9.0.0
 synopsis:        Core contracts for Keiro packages
 description:
   Stable stream, codec, event-stream, and integration-event contracts
@@ -31,6 +31,7 @@
   default-extensions:
     DeriveAnyClass
     DuplicateRecordFields
+    ImportQualifiedPost
     MultilineStrings
     OverloadedLabels
     OverloadedStrings
@@ -60,7 +61,7 @@
     , bytestring    >=0.11 && <0.13
     , deepseq       >=1.5  && <1.6
     , generic-lens  >=2.2  && <2.4
-    , keiki         >=0.7  && <0.8
+    , keiki         >=0.8  && <0.9
     , kiroku-store  >=0.3  && <0.4
     , lens          >=5.2  && <5.4
     , mmzk-typeid   >=0.7  && <0.8
diff --git a/src/Keiro/Codec/IdDomain.hs b/src/Keiro/Codec/IdDomain.hs
--- a/src/Keiro/Codec/IdDomain.hs
+++ b/src/Keiro/Codec/IdDomain.hs
@@ -7,15 +7,24 @@
     typeIdV7Domain,
     idDomainAcceptsText,
     validateIdDomainText,
+    parseKindIdV7Text,
+    parseKindIdV7Value,
     idDomainTextPattern,
     idDomainSampleText,
   )
 where
 
+import Data.Aeson (Value, withText)
+import Data.Aeson.Types (Parser)
+import Data.KindID (KindID)
+import Data.KindID qualified as KindID
+import Data.KindID.Class (ValidPrefix)
 import Data.List.NonEmpty (NonEmpty (..))
+import Data.Proxy (Proxy (..))
 import Data.Text (Text)
 import Data.Text qualified as T
 import Data.TypeID qualified as TypeID
+import GHC.TypeLits (symbolVal)
 import Keiki.ProjectionDomain
   ( DomainConstructionError,
     TextPattern,
@@ -68,7 +77,13 @@
 -- version check, so both operations are part of this frozen contract.
 validateIdDomainText :: IdDomainContract -> Text -> Either IdDomainFailure ()
 validateIdDomainText contract input = do
-  parsed <- either (Left . IdDomainMalformed . T.pack . show) Right (TypeID.parseText input)
+  parsed <- case TypeID.parseText input of
+    Right value -> Right value
+    Left reason ->
+      case TypeID.parseText (T.toLower input) of
+        Right canonical
+          | TypeID.toText canonical == T.toLower input -> Left IdDomainNonCanonical
+        _ -> Left (IdDomainMalformed (T.pack (show reason)))
   let actualPrefix = TypeID.getPrefix parsed
   if actualPrefix == idDomainPrefix contract
     then pure ()
@@ -77,6 +92,31 @@
     then pure ()
     else Left IdDomainNonCanonical
   maybe (Right ()) (Left . IdDomainNotUuidV7 . T.pack . show) (TypeID.checkTypeID parsed)
+
+-- | Parse a canonical TypeID-v7 whose prefix is reflected in the result type.
+-- Keiro's frozen admission policy runs before the dependency constructs the
+-- prefix-indexed value, so generated consumers cannot accidentally widen it.
+parseKindIdV7Text :: forall prefix. (ValidPrefix prefix) => Text -> Either IdDomainFailure (KindID prefix)
+parseKindIdV7Text input = do
+  validateIdDomainText (typeIdV7Domain expectedPrefix) input
+  either (Left . IdDomainMalformed . T.pack . show) Right (KindID.parseText @prefix input)
+  where
+    expectedPrefix = T.pack (symbolVal (Proxy @prefix))
+
+-- | Aeson parser for generated integration-contract fields. When used with
+-- @explicitParseField@, Aeson attaches the owning field key to these stable
+-- Keiro admission failures.
+parseKindIdV7Value :: forall prefix. (ValidPrefix prefix) => Value -> Parser (KindID prefix)
+parseKindIdV7Value = withText "KindID" $ \input ->
+  either (fail . T.unpack . renderIdDomainFailure) pure (parseKindIdV7Text @prefix input)
+
+renderIdDomainFailure :: IdDomainFailure -> Text
+renderIdDomainFailure failure = case failure of
+  IdDomainNonCanonical -> "TypeID text is not canonical lowercase"
+  IdDomainWrongPrefix expected actual ->
+    "TypeID prefix mismatch: expected '" <> expected <> "', found '" <> actual <> "'"
+  IdDomainMalformed reason -> "malformed TypeID text: " <> reason
+  IdDomainNotUuidV7 reason -> "TypeID suffix is not UUIDv7: " <> reason
 
 idDomainTextPattern :: IdDomainContract -> Either DomainConstructionError TextPattern
 idDomainTextPattern contract = do
