diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog for hs-opentelemetry-api-types
+
+## 1.0.0.0 - 2026-05-29
+
+- Initial public release as part of the hs-opentelemetry 1.0 release. Provides
+  the core `Attribute`, `PrimitiveAttribute`, and `AttributeKey` types shared by
+  `hs-opentelemetry-api` and `hs-opentelemetry-semantic-conventions`.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Ian Duncan (c) 2021-2026
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Ian Duncan nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/hs-opentelemetry-api-types.cabal b/hs-opentelemetry-api-types.cabal
new file mode 100644
--- /dev/null
+++ b/hs-opentelemetry-api-types.cabal
@@ -0,0 +1,38 @@
+cabal-version: 2.4
+
+name:        hs-opentelemetry-api-types
+version:     1.0.0.0
+synopsis:    Core attribute types for hs-opentelemetry
+description:
+  Leaf package containing 'Attribute', 'PrimitiveAttribute', and 'AttributeKey'
+  — the foundational value types of the OpenTelemetry API. Extracted so that
+  @hs-opentelemetry-semantic-conventions@ and @hs-opentelemetry-api@ can both
+  depend on these types without creating a circular dependency.
+
+category:    OpenTelemetry, Telemetry, Monitoring, Observability
+homepage:    https://github.com/iand675/hs-opentelemetry#readme
+bug-reports: https://github.com/iand675/hs-opentelemetry/issues
+author:      Ian Duncan
+maintainer:  ian@iankduncan.com
+copyright:   2024 Ian Duncan, Mercury Technologies
+license:     BSD-3-Clause
+license-file: LICENSE
+build-type:  Simple
+extra-doc-files: CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/iand675/hs-opentelemetry
+
+library
+  exposed-modules:
+      OpenTelemetry.Attributes.Attribute
+      OpenTelemetry.Attributes.Key
+  hs-source-dirs: src
+  default-language: Haskell2010
+  ghc-options: -Wall
+  build-depends:
+      base >= 4.7 && < 5
+    , hashable
+    , template-haskell
+    , text
diff --git a/src/OpenTelemetry/Attributes/Attribute.hs b/src/OpenTelemetry/Attributes/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/src/OpenTelemetry/Attributes/Attribute.hs
@@ -0,0 +1,245 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DefaultSignatures #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DeriveLift #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE StrictData #-}
+{-# LANGUAGE TypeFamilies #-}
+
+{- |
+Module      :  OpenTelemetry.Attributes.Attribute
+Copyright   :  (c) Ian Duncan, 2021
+License     :  BSD-3
+Description :  Metadata values
+Maintainer  :  Ian Duncan
+Stability   :  experimental
+Portability :  non-portable (GHC extensions)
+-}
+module OpenTelemetry.Attributes.Attribute (
+  Attribute (..),
+  ToAttribute (..),
+  FromAttribute (..),
+  PrimitiveAttribute (..),
+  ToPrimitiveAttribute (..),
+  FromPrimitiveAttribute (..),
+) where
+
+import Data.Data (Data)
+import Data.Hashable (Hashable)
+import Data.Int (Int64)
+import qualified Data.List as L
+import Data.String (IsString (..))
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import qualified Language.Haskell.TH.Syntax as TH
+import Prelude hiding (lookup, map)
+
+
+-- | Convert a Haskell value to a 'PrimitiveAttribute' value.
+class ToPrimitiveAttribute a where
+  toPrimitiveAttribute :: a -> PrimitiveAttribute
+
+
+class FromPrimitiveAttribute a where
+  fromPrimitiveAttribute :: PrimitiveAttribute -> Maybe a
+
+
+{- | An attribute represents user-provided metadata about a span, link, or event.
+
+ Telemetry tools may use this data to support high-cardinality querying, visualization
+ in waterfall diagrams, trace sampling decisions, and more.
+-}
+data Attribute
+  = -- | An attribute representing a single primitive value
+    AttributeValue PrimitiveAttribute
+  | {- | An attribute representing an array of primitive values.
+
+    All values in the array MUST be of the same primitive attribute type.
+    -}
+    AttributeArray [PrimitiveAttribute]
+  deriving stock (Read, Show, Eq, Ord, Data, Generic, TH.Lift)
+  deriving anyclass (Hashable)
+
+
+{- | Create a `TextAttribute` from the string value.
+
+ @since 0.0.2.1
+-}
+instance IsString PrimitiveAttribute where
+  fromString = TextAttribute . fromString
+
+
+{- | Create a `TextAttribute` from the string value.
+
+ @since 0.0.2.1
+-}
+instance IsString Attribute where
+  fromString = AttributeValue . fromString
+
+
+data PrimitiveAttribute
+  = TextAttribute Text
+  | BoolAttribute Bool
+  | DoubleAttribute {-# UNPACK #-} !Double
+  | IntAttribute {-# UNPACK #-} !Int64
+  deriving stock (Read, Show, Eq, Ord, Data, Generic, TH.Lift)
+  deriving anyclass (Hashable)
+
+
+{- | Convert a Haskell value to an 'Attribute' value.
+
+ For most values, you can define an instance of 'ToPrimitiveAttribute' and use the default 'toAttribute' implementation:
+
+ @
+
+ data Foo = Foo
+
+ instance ToPrimitiveAttribute Foo where
+   toPrimitiveAttribute Foo = TextAttribute "Foo"
+ instance ToAttribute foo
+
+ @
+-}
+class ToAttribute a where
+  toAttribute :: a -> Attribute
+  default toAttribute :: (ToPrimitiveAttribute a) => a -> Attribute
+  toAttribute = AttributeValue . toPrimitiveAttribute
+  {-# INLINE [0] toAttribute #-}
+
+
+class FromAttribute a where
+  fromAttribute :: Attribute -> Maybe a
+  default fromAttribute :: (FromPrimitiveAttribute a) => Attribute -> Maybe a
+  fromAttribute (AttributeValue v) = fromPrimitiveAttribute v
+  fromAttribute _ = Nothing
+
+
+instance ToPrimitiveAttribute PrimitiveAttribute where
+  toPrimitiveAttribute = id
+
+
+instance FromPrimitiveAttribute PrimitiveAttribute where
+  fromPrimitiveAttribute = Just
+
+
+instance ToAttribute PrimitiveAttribute where
+  toAttribute = AttributeValue
+  {-# INLINE [0] toAttribute #-}
+
+
+instance FromAttribute PrimitiveAttribute where
+  fromAttribute (AttributeValue v) = Just v
+  fromAttribute _ = Nothing
+
+
+instance ToPrimitiveAttribute Text where
+  toPrimitiveAttribute = TextAttribute
+
+
+instance FromPrimitiveAttribute Text where
+  fromPrimitiveAttribute (TextAttribute v) = Just v
+  fromPrimitiveAttribute _ = Nothing
+
+
+instance ToAttribute Text
+
+
+instance FromAttribute Text
+
+
+instance ToPrimitiveAttribute Bool where
+  toPrimitiveAttribute = BoolAttribute
+
+
+instance FromPrimitiveAttribute Bool where
+  fromPrimitiveAttribute (BoolAttribute v) = Just v
+  fromPrimitiveAttribute _ = Nothing
+
+
+instance ToAttribute Bool
+
+
+instance FromAttribute Bool
+
+
+instance ToPrimitiveAttribute Double where
+  toPrimitiveAttribute = DoubleAttribute
+
+
+instance FromPrimitiveAttribute Double where
+  fromPrimitiveAttribute (DoubleAttribute v) = Just v
+  fromPrimitiveAttribute _ = Nothing
+
+
+instance ToAttribute Double
+
+
+instance FromAttribute Double
+
+
+instance ToPrimitiveAttribute Int64 where
+  toPrimitiveAttribute = IntAttribute
+
+
+instance FromPrimitiveAttribute Int64 where
+  fromPrimitiveAttribute (IntAttribute v) = Just v
+  fromPrimitiveAttribute _ = Nothing
+
+
+instance ToAttribute Int64
+
+
+instance FromAttribute Int64
+
+
+instance ToPrimitiveAttribute Int where
+  toPrimitiveAttribute = IntAttribute . fromIntegral
+
+
+instance FromPrimitiveAttribute Int where
+  fromPrimitiveAttribute (IntAttribute v) = Just $ fromIntegral v
+  fromPrimitiveAttribute _ = Nothing
+
+
+instance ToAttribute Int
+
+
+instance FromAttribute Int
+
+
+instance ToAttribute Attribute where
+  toAttribute = id
+  {-# INLINE [0] toAttribute #-}
+
+
+instance FromAttribute Attribute where
+  fromAttribute = Just
+
+
+instance (ToPrimitiveAttribute a) => ToAttribute [a] where
+  toAttribute = AttributeArray . L.map toPrimitiveAttribute
+  {-# INLINEABLE toAttribute #-}
+
+
+instance (FromPrimitiveAttribute a) => FromAttribute [a] where
+  fromAttribute (AttributeArray arr) = traverse fromPrimitiveAttribute arr
+  fromAttribute _ = Nothing
+
+
+-- Bypass the two-dictionary chain (ToAttribute → ToPrimitiveAttribute → constructor)
+-- when the value type is statically known. Helps across module boundaries where GHC's
+-- specializer might not fire.
+{-# RULES
+"toAttribute/Text" forall (v :: Text). toAttribute v = AttributeValue (TextAttribute v)
+"toAttribute/Int64" forall (v :: Int64). toAttribute v = AttributeValue (IntAttribute v)
+"toAttribute/Double" forall (v :: Double). toAttribute v = AttributeValue (DoubleAttribute v)
+"toAttribute/Bool" forall (v :: Bool). toAttribute v = AttributeValue (BoolAttribute v)
+"toAttribute/Int" forall (v :: Int). toAttribute v = AttributeValue (IntAttribute (fromIntegral v))
+"toAttribute/Attribute" forall (v :: Attribute). toAttribute v = v
+"toAttribute/PrimAttr" forall (v :: PrimitiveAttribute). toAttribute v = AttributeValue v
+  #-}
diff --git a/src/OpenTelemetry/Attributes/Key.hs b/src/OpenTelemetry/Attributes/Key.hs
new file mode 100644
--- /dev/null
+++ b/src/OpenTelemetry/Attributes/Key.hs
@@ -0,0 +1,48 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+{- |
+Module      :  OpenTelemetry.Attributes.Key
+Copyright   :  (c) Ian Duncan, 2021
+License     :  BSD-3
+Description :  Names for key-value pair metadata
+Maintainer  :  Ian Duncan
+Stability   :  experimental
+Portability :  non-portable (GHC extensions)
+-}
+module OpenTelemetry.Attributes.Key (
+  AttributeKey (..),
+  forget,
+) where
+
+import Data.String (IsString (..))
+import Data.Text (Text)
+import qualified Data.Text as T
+import GHC.Generics (Generic)
+import OpenTelemetry.Attributes.Attribute (Attribute)
+
+
+{- | A 'AttributeKey' is a name for an attribute. The type parameter sets the type of the
+attribute the key should be associated with. This is useful for standardising
+attribute keys, since we can define both the key and the type of value it is
+intended to record.
+
+For example, we might define:
+
+@
+-- See https://opentelemetry.io/docs/specs/semconv/attributes-registry/server/
+serverPortKey :: AttributeKey Int
+serverPortKey = "server.port"
+@
+-}
+newtype AttributeKey a = AttributeKey {unkey :: Text} deriving stock (Show, Eq, Ord, Generic)
+
+
+-- | Raise an error if the string is empty.
+instance IsString (AttributeKey a) where
+  fromString "" = error "AttributeKey cannot be empty"
+  fromString s = AttributeKey $ T.pack s
+
+
+forget :: AttributeKey a -> AttributeKey Attribute
+forget = AttributeKey . unkey
