diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Revision history for grpc-spec
 
+## 1.1.1 -- 2026-10-30
+
+* Support for (non-standard) CBOR RPC [#383, Finley McIlwaine]
+
 ## 1.1.0 -- 2026-09-02
 
 * Trivial `BuildMetadata` and `ParseMetadata` instances for `[CustomMetadata]`
diff --git a/grpc-spec.cabal b/grpc-spec.cabal
--- a/grpc-spec.cabal
+++ b/grpc-spec.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               grpc-spec
-version:            1.1.0
+version:            1.1.1
 synopsis:           Implementation of the pure part of the gRPC spec
 description:        This is an implementation of the pure part of the core gRPC
                     spec at <https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md>.
@@ -105,6 +105,7 @@
       Network.GRPC.Spec.OrcaLoadReport
       Network.GRPC.Spec.PercentEncoding
       Network.GRPC.Spec.RPC
+      Network.GRPC.Spec.RPC.CBOR
       Network.GRPC.Spec.RPC.JSON
       Network.GRPC.Spec.RPC.Protobuf
       Network.GRPC.Spec.RPC.Raw
@@ -134,6 +135,7 @@
     , binary                    >= 0.8     && < 0.9
     , bytestring                >= 0.10.12 && < 0.13
     , case-insensitive          >= 1.2     && < 1.3
+    , cborg                     >= 0.2     && < 0.3
     , containers                >= 0.6     && < 0.9
     , data-default              >= 0.7     && < 0.9
     , deepseq                   >= 1.4     && < 1.6
@@ -147,6 +149,7 @@
     , proto-lens-protobuf-types >= 0.7     && < 0.8
     , proto-lens-runtime        >= 0.7     && < 0.8
     , record-hasfield           >= 1.0     && < 1.1
+    , serialise                 >= 0.2     && < 0.3
     , text                      >= 1.2     && < 2.2
     , utf8-string               >= 1.0     && < 1.1
     , vector                    >= 0.13    && < 0.14
@@ -198,7 +201,7 @@
       -- Additional dependencies
     , prettyprinter               >= 1.7   && < 1.8
     , prettyprinter-ansi-terminal >= 1.1   && < 1.2
-    , QuickCheck                  >= 2.14  && < 2.19
+    , QuickCheck                  >= 2.14  && < 2.20
     , quickcheck-instances        >= 0.3   && < 0.5
     , tasty                       >= 1.4   && < 1.6
     , tasty-quickcheck            >= 0.10  && < 0.12
diff --git a/src/Network/GRPC/Spec.hs b/src/Network/GRPC/Spec.hs
--- a/src/Network/GRPC/Spec.hs
+++ b/src/Network/GRPC/Spec.hs
@@ -27,6 +27,8 @@
   , EncodeFields -- opaque
     -- *** Raw
   , RawRpc
+    -- *** CBOR
+  , CborRpc
     -- * Streaming types
   , StreamingType(..)
   , SStreamingType(..)
@@ -187,6 +189,7 @@
 import Network.GRPC.Spec.MessageMeta
 import Network.GRPC.Spec.OrcaLoadReport
 import Network.GRPC.Spec.RPC
+import Network.GRPC.Spec.RPC.CBOR
 import Network.GRPC.Spec.RPC.JSON
 import Network.GRPC.Spec.RPC.Protobuf
 import Network.GRPC.Spec.RPC.Raw
diff --git a/src/Network/GRPC/Spec/RPC/CBOR.hs b/src/Network/GRPC/Spec/RPC/CBOR.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/GRPC/Spec/RPC/CBOR.hs
@@ -0,0 +1,93 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Network.GRPC.Spec.RPC.CBOR (CborRpc) where
+
+import Codec.CBOR.Read qualified as CBOR
+import Codec.Serialise
+import Control.DeepSeq (NFData)
+import Control.Exception (displayException)
+import Data.ByteString.Char8 qualified as BS.Char8
+import Data.ByteString.Lazy qualified as BS.Lazy
+import Data.ByteString.Lazy qualified as Lazy (ByteString)
+import Data.Proxy
+import GHC.TypeLits
+
+import Network.GRPC.Spec.CustomMetadata.Typed
+import Network.GRPC.Spec.RPC
+import Network.GRPC.Spec.RPC.StreamType
+
+{-------------------------------------------------------------------------------
+  CBOR format
+-------------------------------------------------------------------------------}
+
+-- | gRPC using CBOR as the message encoding
+--
+-- You will need to manually provide 'Input' and 'Output' instances for each
+-- RPC you use, along with 'Serialise' instances for those types.
+data CborRpc (serv :: Symbol) (meth :: Symbol)
+
+instance ( KnownSymbol serv
+         , KnownSymbol meth
+
+           -- Serialization
+         , NFData (Input  (CborRpc serv meth))
+         , NFData (Output (CborRpc serv meth))
+
+           -- Debugging constraints
+         , Show (Input  (CborRpc serv meth))
+         , Show (Output (CborRpc serv meth))
+         , Show (RequestMetadata (CborRpc serv meth))
+         , Show (ResponseInitialMetadata (CborRpc serv meth))
+         , Show (ResponseTrailingMetadata (CborRpc serv meth))
+         ) => IsRPC (CborRpc serv meth) where
+  rpcContentType _ = defaultRpcContentType "cbor"
+  rpcServiceName _ = BS.Char8.pack $ symbolVal (Proxy @serv)
+  rpcMethodName  _ = BS.Char8.pack $ symbolVal (Proxy @meth)
+  rpcMessageType _ = Nothing
+
+instance ( IsRPC (CborRpc serv meth)
+
+           -- Serialization constraints
+         , Serialise (Input  (CborRpc serv meth))
+         , Serialise (Output (CborRpc serv meth))
+
+           -- Metadata constraints
+         , BuildMetadata (RequestMetadata          (CborRpc serv meth))
+         , ParseMetadata (ResponseInitialMetadata  (CborRpc serv meth))
+         , ParseMetadata (ResponseTrailingMetadata (CborRpc serv meth))
+         ) => SupportsClientRpc (CborRpc serv meth) where
+  rpcSerializeInput    _ = serialise
+  rpcDeserializeOutput _ = deserialiseExactly
+
+instance ( IsRPC (CborRpc serv meth)
+
+           -- Serialization constraints
+         , Serialise (Input  (CborRpc serv meth))
+         , Serialise (Output (CborRpc serv meth))
+
+           -- Metadata constraints
+         , ParseMetadata (RequestMetadata          (CborRpc serv meth))
+         , BuildMetadata (ResponseInitialMetadata  (CborRpc serv meth))
+         , BuildMetadata (ResponseTrailingMetadata (CborRpc serv meth))
+         ) => SupportsServerRpc (CborRpc serv meth) where
+  rpcDeserializeInput _ = deserialiseExactly
+  rpcSerializeOutput  _ = serialise
+
+-- | For CBOR protocol we do not check communication protocols
+instance ValidStreamingType styp
+      => SupportsStreamingType (CborRpc serv meth) styp
+
+-- | Decode the CBOR-encoded value in the given 'Lazy.ByteString'
+--
+-- Fails if the decoder fails or if there are any bytes leftover after a
+-- successful decode.
+deserialiseExactly :: Serialise a => Lazy.ByteString -> Either String a
+deserialiseExactly bs =
+    case CBOR.deserialiseFromBytes decode bs of
+      Left err ->
+        Left $ displayException err
+      Right (unconsumed, a) ->
+        if BS.Lazy.null unconsumed then
+          Right a
+        else
+          Left "Not all bytes consumed"
