diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## 0.2.1.0
+
+### Added
+- `Ord` and `Hashable` instances for `RequestId` for use as container keys.
+- `messageId` returns the ID of a request, response, or error. It returns
+  `Nothing` for a notification and preserves explicit null IDs.
+
 ## 0.2.0
 
 ### Added
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # jsonrpc
 
+[![Hackage](https://img.shields.io/hackage/v/jsonrpc.svg)](https://hackage.haskell.org/package/jsonrpc)
+
 A lightweight Haskell implementation of
 [JSON-RPC 2.0](https://www.jsonrpc.org/specification) protocol types with
 [Aeson](https://hackage.haskell.org/package/aeson) serialisation.
diff --git a/jsonrpc.cabal b/jsonrpc.cabal
--- a/jsonrpc.cabal
+++ b/jsonrpc.cabal
@@ -1,14 +1,14 @@
 cabal-version: 2.2
 name: jsonrpc
-version: 0.2.0.0
+version: 0.2.1.0
 license: MPL-2.0
 license-file: LICENSE
 copyright: (c) 2026 DPella AB
 author: DPella AB
 tested-with: ghc ==9.12.2
 extra-doc-files:
-  README.md
   CHANGELOG.md
+  README.md
 
 maintainer:
   matti@dpella.io,
@@ -56,6 +56,7 @@
   build-depends:
     aeson >=2.1 && <2.3,
     base >=4.18 && <4.22,
+    hashable >=1.4 && <1.6,
     text >=2.0 && <2.2,
 
 test-suite test-jsonrpc
diff --git a/src/JSONRPC.hs b/src/JSONRPC.hs
--- a/src/JSONRPC.hs
+++ b/src/JSONRPC.hs
@@ -75,6 +75,7 @@
 
     -- * Helpers
     EmptyParams,
+    messageId,
 
     -- * Standard JSON-RPC 2.0 error codes
     pARSE_ERROR,
@@ -91,6 +92,7 @@
 import Data.Aeson
 import Data.Aeson qualified as Aeson
 import Data.Data (Proxy (..), Typeable, typeRep)
+import Data.Hashable (Hashable)
 import Data.Kind (Type)
 import Data.Text (Text)
 import GHC.Generics
@@ -106,10 +108,12 @@
 The JSON-RPC 2.0 specification requires that each request has a unique
 identifier.  The ID can be a string, number, or null value.  Numbers
 SHOULD NOT contain fractional parts.
+
+The 'Ord' instance supports ordered containers. The order has no protocol meaning.
 -}
 newtype RequestId = RequestId Value
-    deriving stock (Show, Eq)
-    deriving newtype (ToJSON, FromJSON)
+    deriving stock (Show, Eq, Ord)
+    deriving newtype (ToJSON, FromJSON, Hashable)
 
 -- ---------------------------------------------------------------------------
 -- Constants
@@ -174,8 +178,9 @@
     }
     deriving stock (Show, Eq, Generic)
 
--- | Custom 'ToJSON' that maps the Haskell field @errorData@ to the JSON key @\"data\"@
--- as required by the JSON-RPC 2.0 specification.
+{- | Custom 'ToJSON' that maps the Haskell field @errorData@ to the JSON key @\"data\"@
+as required by the JSON-RPC 2.0 specification.
+-}
 instance ToJSON JSONRPCErrorInfo where
     toJSON (JSONRPCErrorInfo c m d) =
         object $
@@ -215,8 +220,9 @@
     deriving stock (Show, Eq, Generic)
     deriving anyclass (ToJSON)
 
--- | Custom 'FromJSON' instance that treats a missing @params@ key as 'Null'.
--- JSON-RPC 2.0 allows the @params@ field to be omitted.
+{- | Custom 'FromJSON' instance that treats a missing @params@ key as 'Null'.
+JSON-RPC 2.0 allows the @params@ field to be omitted.
+-}
 instance FromJSON JSONRPCRequest where
     parseJSON = withObject "JSONRPCRequest" $ \o ->
         JSONRPCRequest
@@ -284,8 +290,9 @@
     toJSON (JSONRPCNotification j m p) =
         object ["jsonrpc" .= j, "method" .= m, "params" .= p]
 
--- | Custom 'FromJSON' instance that treats a missing @params@ key as 'Null'.
--- JSON-RPC 2.0 allows notifications to omit the @params@ field entirely.
+{- | Custom 'FromJSON' instance that treats a missing @params@ key as 'Null'.
+JSON-RPC 2.0 allows notifications to omit the @params@ field entirely.
+-}
 instance FromJSON JSONRPCNotification where
     parseJSON = withObject "JSONRPCNotification" $ \o ->
         JSONRPCNotification
@@ -304,6 +311,18 @@
     | NotificationMessage JSONRPCNotification
     deriving stock (Show, Eq, Generic)
 
+{- | Return the ID of a request, response, or error. Return 'Nothing' for a notification.
+
+A request carries its own ID. A response or error carries the ID it answers.
+An explicit null ID returns @Just (RequestId Null)@.
+-}
+messageId :: JSONRPCMessage -> Maybe RequestId
+messageId = \case
+    RequestMessage JSONRPCRequest{id = rid} -> Just rid
+    ResponseMessage JSONRPCResponse{id = rid} -> Just rid
+    ErrorMessage JSONRPCError{id = rid} -> Just rid
+    NotificationMessage _ -> Nothing
+
 instance ToJSON JSONRPCMessage where
     toJSON (RequestMessage r) = toJSON r
     toJSON (ResponseMessage r) = toJSON r
@@ -349,6 +368,7 @@
     where
     -- | The type of the @params@ field.
     type RequestParams a
+
     type RequestParams a = RequestParamType (Rep a)
 
     -- | The JSON-RPC method name (e.g. @\"initialize\"@, @\"tools\/list\"@).
@@ -418,6 +438,7 @@
     where
     -- | The type of the @params@ field.
     type NotificationParams a
+
     type NotificationParams a = NotificationParamType (Rep a)
 
     -- | The JSON-RPC method name.
