diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Richard Marko (c) 2018
+
+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 Author name here 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/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src/Data/TTN.hs b/src/Data/TTN.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TTN.hs
@@ -0,0 +1,5 @@
+module Data.TTN (
+    module Data.TTN.Types
+  ) where
+
+import Data.TTN.Types
diff --git a/src/Data/TTN/Types.hs b/src/Data/TTN/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/TTN/Types.hs
@@ -0,0 +1,384 @@
+{-# LANGUAGE RecordWildCards     #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE DeriveGeneric       #-}
+
+module Data.TTN.Types where
+
+import System.Exit (exitFailure, exitSuccess)
+import System.IO (stderr, hPutStrLn)
+import qualified Data.ByteString.Char8 as B
+import System.Environment (getArgs)
+import Control.Monad (forM_, mzero, join)
+import Control.Applicative
+import Data.Aeson(decodeStrict, eitherDecodeStrict,
+                  Value(..), FromJSON(..), ToJSON(..),
+                  pairs, withText, (.:), (.:?), (.=), object)
+import Data.Monoid
+import Data.Text (Text, unpack)
+import GHC.Generics
+
+import Data.Time.RFC3339
+import Data.Time.LocalTime
+
+-- | Workaround for https://github.com/bos/aeson/issues/287.
+o .:?? val = fmap join (o .:? val)
+
+data TTNZonedTime = TTNZonedTime { unwrap :: ZonedTime } deriving (Show, Generic)
+
+instance ToJSON TTNZonedTime where
+  toJSON     = toJSON . (formatTimeRFC3339 :: ZonedTime -> Text) . unwrap
+  toEncoding =  toEncoding . (formatTimeRFC3339 :: ZonedTime -> Text) . unwrap
+
+instance FromJSON TTNZonedTime where
+  parseJSON = withText "TTNZonedTime" $ \x ->
+    case parseTimeRFC3339 x of
+      Nothing -> fail $ "Parsing RFC3339 value failed, got:" ++ show x
+      Just t -> return $ TTNZonedTime t
+
+
+-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/event.go#L65
+data Config = Config {
+    configFrequency  :: Maybe Double
+  , configDataRate   :: Maybe Text
+  , configCounter    :: Maybe Double
+  , configAirtime    :: Maybe Double
+  , configPower      :: Maybe Double
+  , configModulation :: Maybe Text
+  } deriving (Show, Generic)
+
+instance FromJSON Config where
+  parseJSON (Object v) = Config
+    <$> v .:?? "frequency"
+    <*> v .:?? "data_rate"
+    <*> v .:?? "counter"
+    <*> v .:?? "airtime"
+    <*> v .:?? "power"
+    <*> v .:?? "modulation"
+  parseJSON _          = mzero
+
+instance ToJSON Config where
+  toJSON     (Config {..}) = object [
+      "frequency"   .= configFrequency
+    , "data_rate"   .= configDataRate
+    , "counter"     .= configCounter
+    , "airtime"     .= configAirtime
+    , "power"       .= configPower
+    , "modulation"  .= configModulation
+    ]
+  toEncoding (Config {..}) = pairs (
+       "frequency"  .= configFrequency
+    <> "data_rate"  .= configDataRate
+    <> "counter"    .= configCounter
+    <> "airtime"    .= configAirtime
+    <> "power"      .= configPower
+    <> "modulation" .= configModulation
+    )
+
+
+-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/gateway_metadata.go
+-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/location_metadata.go
+data GatewaysElt = GatewaysElt {
+    gatewaysEltGtwId                  :: Maybe Text
+  , gatewaysEltGtwTrusted             :: Maybe Bool
+  , gatewaysEltTimestamp              :: Maybe Int
+  , gatewaysEltFineTimestamp          :: Maybe Int
+  , gatewaysEltFineTimestampEncrypted :: Maybe Text
+  , gatewaysEltTime                   :: Maybe TTNZonedTime
+  , gatewaysEltAntenna                :: Maybe Text
+  , gatewaysEltChannel                :: Double
+  , gatewaysEltRSSI                   :: Double
+  , gatewaysEltSNR                    :: Double
+  , gatewaysEltRFChain                :: Int
+  , gatewaysEltLatitude               :: Maybe Double
+  , gatewaysEltLongitude              :: Maybe Double
+  , gatewaysEltAltitude               :: Maybe Int
+  , gatewaysEltAccuracy               :: Maybe Int
+  , gatewaysEltSource                 :: Maybe Text
+  } deriving (Show, Generic)
+
+instance FromJSON GatewaysElt where
+  parseJSON (Object v) = GatewaysElt
+    <$> v .:?? "gtw_id"
+    <*> v .:?? "gtw_trusted"
+    <*> v .:?? "timestamp"
+    <*> v .:?? "fine_timestamp"
+    <*> v .:?? "fine_timestamp_encrypted"
+    <*> v .:?? "time"
+    <*> v .:?? "antenna"
+    <*> v .:   "channel"
+    <*> v .:   "rssi"
+    <*> v .:   "snr"
+    <*> v .:   "rf_chain"
+    <*> v .:?? "latitude"
+    <*> v .:?? "longitude"
+    <*> v .:?? "altitude"
+    <*> v .:?? "accuracy"
+    <*> v .:?? "source"
+  parseJSON _          = mzero
+
+instance ToJSON GatewaysElt where
+  toJSON     (GatewaysElt {..}) = object [
+      "gtw_id"                    .= gatewaysEltGtwId
+    , "gtw_trusted"               .= gatewaysEltGtwTrusted
+    , "timestamp"                 .= gatewaysEltTimestamp
+    , "fine_timestamp"            .= gatewaysEltFineTimestamp
+    , "fine_timestamp_encrypted"  .= gatewaysEltFineTimestampEncrypted
+    , "time"                      .= gatewaysEltTime
+    , "antenna"                   .= gatewaysEltAntenna
+    , "channel"                   .= gatewaysEltChannel
+    , "rssi"                      .= gatewaysEltRSSI
+    , "snr"                       .= gatewaysEltSNR
+    , "rf_chain"                  .= gatewaysEltRFChain
+    , "latitude"                  .= gatewaysEltLatitude
+    , "longitude"                 .= gatewaysEltLongitude
+    , "altitude"                  .= gatewaysEltAltitude
+    , "accuracy"                  .= gatewaysEltAccuracy
+    , "source"                    .= gatewaysEltSource
+    ]
+  toEncoding (GatewaysElt {..}) = pairs (
+       "gtw_id"                   .= gatewaysEltGtwId
+    <> "gtw_trusted"              .= gatewaysEltGtwTrusted
+    <> "timestamp"                .= gatewaysEltTimestamp
+    <> "fine_timestamp"           .= gatewaysEltFineTimestamp
+    <> "fine_timestamp_encrypted" .= gatewaysEltFineTimestampEncrypted
+    <> "time"                     .= gatewaysEltTime
+    <> "antenna"                  .= gatewaysEltAntenna
+    <> "channel"                  .= gatewaysEltChannel
+    <> "rssi"                     .= gatewaysEltRSSI
+    <> "snr"                      .= gatewaysEltSNR
+    <> "rf_chain"                 .= gatewaysEltRFChain
+    <> "latitude"                 .= gatewaysEltLatitude
+    <> "longitude"                .= gatewaysEltLongitude
+    <> "altitude"                 .= gatewaysEltAltitude
+    <> "accuracy"                 .= gatewaysEltAccuracy
+    <> "source"                   .= gatewaysEltSource
+    )
+
+
+-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/metadata.go
+data Metadata = Metadata {
+    metadataTime       :: Maybe TTNZonedTime
+  , metadataFrequency  :: Maybe Double
+  , metadataModulation :: Maybe Text
+  , metadataDataRate   :: Maybe Text
+  , metadataBitRate    :: Maybe Int
+  , metadataAirtime    :: Maybe Double
+  , metadataCodingRate :: Maybe Text
+  , metadataGateways   :: [GatewaysElt]
+  } deriving (Show, Generic)
+
+instance FromJSON Metadata where
+  parseJSON (Object v) = Metadata <$>
+        v .:?? "time"
+    <*> v .:?? "frequency"
+    <*> v .:?? "modulation"
+    <*> v .:?? "data_rate"
+    <*> v .:?? "bit_rate"
+    <*> v .:?? "airtime"
+    <*> v .:?? "coding_rate"
+    <*> v .:   "gateways"
+  parseJSON _          = mzero
+
+instance ToJSON Metadata where
+  toJSON     (Metadata {..}) = object [
+      "time"         .= metadataTime
+    , "frequency"    .= metadataFrequency
+    , "modulation"   .= metadataModulation
+    , "data_rate"    .= metadataDataRate
+    , "bit_rate"     .= metadataBitRate
+    , "coding_rate"  .= metadataCodingRate
+    , "airtime"      .= metadataAirtime
+    , "gateways"     .= metadataGateways
+    ]
+  toEncoding (Metadata {..}) = pairs (
+       "time" .= metadataTime
+    <> "frequency"   .= metadataFrequency
+    <> "modulation"  .= metadataModulation
+    <> "data_rate"   .= metadataDataRate
+    <> "bit_rate"    .= metadataBitRate
+    <> "coding_rate" .= metadataCodingRate
+    <> "airtime"     .= metadataAirtime
+    <> "gateways"    .= metadataGateways
+    )
+
+
+data Message = Message {
+    messageDevId :: Text,
+    messageAppId :: Text,
+    messagePort  :: Double
+  } deriving (Show, Generic)
+
+instance FromJSON Message where
+  parseJSON (Object v) = Message
+    <$> v .: "dev_id"
+    <*> v .: "app_id"
+    <*> v .: "port"
+  parseJSON _          = mzero
+
+instance ToJSON Message where
+  toJSON     (Message {..}) = object [
+      "dev_id"  .= messageDevId
+    , "app_id"  .= messageAppId
+    , "port"    .= messagePort
+    ]
+  toEncoding (Message {..}) = pairs (
+       "dev_id" .= messageDevId
+    <> "app_id" .= messageAppId
+    <> "port"   .= messagePort
+    )
+
+
+-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/uplink_message.go
+data Uplink = Uplink {
+    uplinkConfig         :: Maybe Config
+  , uplinkGatewayId      :: Maybe Text
+  , uplinkDevId          :: Maybe Text
+  , uplinkPayload        :: Maybe Text
+  , uplinkCounter        :: Maybe Double
+  , uplinkIsRetry        :: Maybe Bool
+  , uplinkMetadata       :: Maybe Metadata
+  , uplinkPayloadRaw     :: Maybe Text
+  , uplinkMessage        :: Maybe Message
+  , uplinkAppId          :: Maybe Text
+  , uplinkConfirmed      :: Maybe Bool
+  , uplinkHardwareSerial :: Maybe Text
+  , uplinkPort           :: Maybe Double
+  } deriving (Show, Generic)
+
+instance FromJSON Uplink where
+  parseJSON (Object v) = Uplink
+    <$> v .:?? "config"
+    <*> v .:?? "gateway_id"
+    <*> v .:?? "dev_id"
+    <*> v .:?? "payload"
+    <*> v .:?? "counter"
+    <*> v .:?? "is_retry"
+    <*> v .:?? "metadata"
+    <*> v .:?? "payload_raw"
+    <*> v .:?? "message"
+    <*> v .:?? "app_id"
+    <*> v .:?? "confirmed"
+    <*> v .:?? "hardware_serial"
+    <*> v .:?? "port"
+  parseJSON _          = mzero
+
+instance ToJSON Uplink where
+  toJSON     (Uplink {..}) = object [
+      "config"           .= uplinkConfig
+    , "gateway_id"       .= uplinkGatewayId
+    , "dev_id"           .= uplinkDevId
+    , "payload"          .= uplinkPayload
+    , "counter"          .= uplinkCounter
+    , "is_retry"         .= uplinkIsRetry
+    , "metadata"         .= uplinkMetadata
+    , "payload_raw"      .= uplinkPayloadRaw
+    , "message"          .= uplinkMessage
+    , "app_id"           .= uplinkAppId
+    , "confirmed"        .= uplinkConfirmed
+    , "hardware_serial"  .= uplinkHardwareSerial
+    , "port"             .= uplinkPort
+    ]
+  toEncoding (Uplink {..}) = pairs (
+       "config"          .= uplinkConfig
+    <> "gateway_id"      .= uplinkGatewayId
+    <> "dev_id"          .= uplinkDevId
+    <> "payload"         .= uplinkPayload
+    <> "counter"         .= uplinkCounter
+    <> "is_retry"        .= uplinkIsRetry
+    <> "metadata"        .= uplinkMetadata
+    <> "payload_raw"     .= uplinkPayloadRaw
+    <> "message"         .= uplinkMessage
+    <> "app_id"          .= uplinkAppId
+    <> "confirmed"       .= uplinkConfirmed
+    <> "hardware_serial" .= uplinkHardwareSerial
+    <> "port"            .= uplinkPort
+    )
+
+data Schedule = ScheduleReplace | ScheduleFirst | ScheduleLast
+  deriving (Eq, Show)
+
+instance FromJSON Schedule where
+  parseJSON = withText "Schedule" $ \x ->
+    case x of
+      "replace" -> return ScheduleReplace
+      "first"   -> return ScheduleFirst
+      "last"    -> return ScheduleLast
+      w         -> fail $ "Unknown schedule" ++ (unpack w)
+
+instance ToJSON Schedule where
+  toJSON ScheduleReplace = "replace"
+  toJSON ScheduleFirst   = "first"
+  toJSON ScheduleLast    = "last"
+
+-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/downlink_message.go
+data Downlink = Downlink {
+    downlinkAppId          :: Text
+  , downlinkDevId          :: Text
+  , downlinkPort           :: Double
+  , downlinkPayloadRaw     :: Text
+  , downlinkConfirmed      :: Bool
+  , downlinkSchedule       :: Maybe Schedule
+  } deriving (Show, Eq, Generic)
+
+instance FromJSON Downlink where
+  parseJSON (Object v) = Downlink
+    <$> v .:   "app_id"
+    <*> v .:   "dev_id"
+    <*> v .:   "port"
+    <*> v .:   "payload_raw"
+    <*> v .:   "confirmed"
+    <*> v .:?? "schedule"
+  parseJSON _          = mzero
+
+instance ToJSON Downlink where
+  toJSON     (Downlink {..}) = object [
+      "app_id"           .= downlinkAppId
+    , "dev_id"           .= downlinkDevId
+    , "port"             .= downlinkPort
+    , "payload_raw"      .= downlinkPayloadRaw
+    , "confirmed"        .= downlinkConfirmed
+    , "schedule"         .= downlinkSchedule
+    ]
+  toEncoding (Downlink {..}) = pairs (
+       "app_id"          .= downlinkAppId
+    <> "dev_id"          .= downlinkDevId
+    <> "port"            .= downlinkPort
+    <> "payload_raw"     .= downlinkPayloadRaw
+    <> "confirmed"       .= downlinkConfirmed
+    <> "schedule"        .= downlinkSchedule
+    )
+
+
+data Error = Error { errorMsg :: Text } deriving (Show)
+
+instance FromJSON Error where
+  parseJSON (Object v) = Error <$> v .: "error"
+  parseJSON _          = mzero
+
+instance ToJSON Error where
+  toJSON (Error {..}) = object [ "error" .= errorMsg ]
+  toEncoding (Error {..}) = pairs ( "error" .= errorMsg )
+
+parse :: B.ByteString -> Either String Uplink
+parse = eitherDecodeStrict
+
+parseError :: B.ByteString -> Either String Error
+parseError = eitherDecodeStrict
+
+parseFile :: FilePath -> IO Uplink
+parseFile filename = do
+  input <- B.readFile filename
+  case decodeStrict input of
+    Nothing -> fatal $ case (decodeStrict input :: Maybe Value) of
+                         Nothing -> "Invalid JSON file: "     ++ filename
+                         Just v  -> "Mismatched JSON value from file: " ++ filename
+    Just r  -> return (r :: Uplink)
+  where
+    fatal :: String -> IO a
+    fatal msg = do hPutStrLn stderr msg
+                   exitFailure
+
+parseMany :: [String] -> IO ()
+parseMany filenames = do
+  forM_ filenames (\f -> parseFile f >>= (\p -> p `seq` putStrLn $ "Successfully parsed " ++ f))
+  exitSuccess
diff --git a/test/DownlinkSpec.hs b/test/DownlinkSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/DownlinkSpec.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module DownlinkSpec where
+
+import SpecHelper
+import Data.Aeson
+
+spec :: Spec
+spec = do
+  it "parses downlink" $ do
+    let d = Downlink {
+              downlinkAppId = "APP"
+            , downlinkDevId = "DEV"
+            , downlinkPort  = 0
+            , downlinkPayloadRaw = "payload"
+            , downlinkConfirmed = False
+            , downlinkSchedule = Just ScheduleFirst
+            }
+
+    (decode . encode $ d) `shouldBe` (Just d)
+
+main :: IO ()
+main = hspec spec
diff --git a/test/ParseSpec.hs b/test/ParseSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/ParseSpec.hs
@@ -0,0 +1,208 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module ParseSpec where
+
+import Text.RawString.QQ
+
+import SpecHelper
+
+spec :: Spec
+spec = do
+  it "parses samples" $ do
+    mapM_ (\x -> parse x `shouldSatisfy` isRight) [p1, p2, p3, p4, p5, p6]
+
+main :: IO ()
+main = hspec spec
+
+p1 = [r|
+{
+  "payload": "YPkvASYgAAAemoR/",
+  "message": {
+    "app_id": "basetest",
+    "dev_id": "tester",
+    "port": 0
+  },
+  "gateway_id": "eui-b827ebfffe114355",
+  "config": {
+    "modulation": "LORA",
+    "data_rate": "SF9BW125",
+    "airtime": 144384000,
+    "frequency": 869525000,
+    "power": 27
+  }
+}
+|]
+
+p2 = [r|
+{
+  "app_id": "basetest",
+  "dev_id": "tester",
+  "hardware_serial": "0004A30B001E0D4D",
+  "port": 1,
+  "counter": 1276,
+  "confirmed": true,
+  "payload_raw": "jZOoQaCyMkI=",
+  "metadata": {
+    "time": "2018-02-09T19:10:19.912012664Z",
+    "frequency": 867.3,
+    "modulation": "LORA",
+    "data_rate": "SF12BW125",
+    "airtime": 1482752000,
+    "coding_rate": "4/5",
+    "gateways": [
+      {
+        "gtw_id": "eui-b827ebfffec6e5d0",
+        "timestamp": 2377743228,
+        "time": "2018-02-09T19:10:19.884395Z",
+        "channel": 4,
+        "rssi": -83,
+        "snr": 8.5,
+        "rf_chain": 0,
+        "latitude": 49.19936,
+        "longitude": 16.57975,
+        "altitude": 320
+      },
+      {
+        "gtw_id": "eui-1dee0933d840ee1b",
+        "timestamp": 4138754052,
+        "time": "2018-02-09T19:16:34.416079Z",
+        "channel": 4,
+        "rssi": -115,
+        "snr": -16,
+        "rf_chain": 0
+      }
+    ]
+  }
+}
+|]
+
+p3 = [r|
+{
+  "payload": "YO0qASYgmwblb5If",
+  "message": {
+    "app_id": "basetest",
+    "dev_id": "tester",
+    "port": 0
+  },
+  "gateway_id": "eui-b827ebfffec6e5d0",
+  "config": {
+    "modulation": "LORA",
+    "data_rate": "SF9BW125",
+    "airtime": 144384000,
+    "counter": 1691,
+    "frequency": 869525000,
+    "power": 27
+  }
+}
+|]
+
+p4 = [r|
+{
+  "app_id": "basetest",
+  "dev_id": "tester",
+  "hardware_serial": "0004A30B001E0D4D",
+  "port": 1,
+  "counter": 1276,
+  "confirmed": true,
+  "payload_raw": "jZOoQaCyMkI=",
+  "metadata": {
+    "time": "2018-02-09T19:10:19.912012664Z",
+    "frequency": 867.3,
+    "modulation": "LORA",
+    "data_rate": "SF12BW125",
+    "airtime": 1482752000,
+    "coding_rate": "4/5",
+    "gateways": [
+      {
+        "gtw_id": "eui-b827ebfffec6e5d0",
+        "timestamp": 2377743228,
+        "time": "2018-02-09T19:10:19.884395Z",
+        "channel": 4,
+        "rssi": -83,
+        "snr": 8.5,
+        "rf_chain": 0,
+        "latitude": 49.19936,
+        "longitude": 16.57975,
+        "altitude": 320
+      },
+      {
+        "gtw_id": "eui-1dee0933d840ee1b",
+        "timestamp": 4138754052,
+        "time": "2018-02-09T19:16:34.416079Z",
+        "channel": 4,
+        "rssi": -115,
+        "snr": -16,
+        "rf_chain": 0
+      }
+    ]
+  }
+}
+|]
+
+p5 = [r|
+{
+  "app_id": "basetest",
+  "dev_id": "tester",
+  "hardware_serial": "0004A30B001E0D4D",
+  "port": 1,
+  "counter": 0,
+  "confirmed": true,
+  "is_retry": true,
+  "payload_raw": "SA==",
+  "metadata": {
+    "time": "2017-10-27T20:31:23.636977619Z",
+    "frequency": 867.5,
+    "modulation": "LORA",
+    "data_rate": "SF12BW125",
+    "coding_rate": "4/5",
+    "gateways": [
+      {
+        "gtw_id": "eui-b827ebfffec6e5d0",
+        "timestamp": 2729574940,
+        "time": "2017-10-27T20:31:23.3235Z",
+        "channel": 5,
+        "rssi": -112,
+        "snr": -9.2,
+        "rf_chain": 0,
+        "latitude": 49.19936,
+        "longitude": 16.57975,
+        "altitude": 320
+      }
+    ]
+  }
+}
+|]
+
+p6 = [r|
+{
+  "app_id": "basetest",
+  "dev_id": "tester",
+  "hardware_serial": "0004A30B001E0D4D",
+  "port": 1,
+  "counter": 3,
+  "confirmed": true,
+  "payload_raw": "8mmxQVBmM0I=",
+  "metadata": {
+    "time": "2017-10-28T03:43:47.534601976Z",
+    "frequency": 867.1,
+    "modulation": "LORA",
+    "data_rate": "SF12BW125",
+    "coding_rate": "4/5",
+    "gateways": [
+      {
+        "gtw_id": "eui-b827ebfffec6e5d0",
+        "timestamp": 2903809348,
+        "time": "2017-10-28T03:43:47.361855Z",
+        "channel": 3,
+        "rssi": -119,
+        "snr": -10.5,
+        "rf_chain": 0,
+        "latitude": 49.19936,
+        "longitude": 16.57975,
+        "altitude": 320
+      }
+    ]
+  }
+}
+|]
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs
new file mode 100644
--- /dev/null
+++ b/test/SpecHelper.hs
@@ -0,0 +1,9 @@
+module SpecHelper
+    ( module Test.Hspec
+    , module Test.Hspec.Expectations.Contrib
+    , module Data.TTN
+    ) where
+
+import Test.Hspec
+import Test.Hspec.Expectations.Contrib
+import Data.TTN
diff --git a/ttn.cabal b/ttn.cabal
new file mode 100644
--- /dev/null
+++ b/ttn.cabal
@@ -0,0 +1,45 @@
+name:                ttn
+version:             0.1.0.0
+synopsis:            Things Tracker Network JSON Types
+description:         Parse JSONs from TTN MQTT API
+homepage:            https://github.com/sorki/data-ttn
+license:             BSD3
+license-file:        LICENSE
+author:              Richard Marko
+maintainer:          srk@48.io
+copyright:           2018 Richard Marko
+category:            Web
+build-type:          Simple
+-- extra-source-files:
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Data.TTN
+                     , Data.TTN.Types
+  build-depends:       base >= 4.7 && < 5
+                     , aeson
+                     , timerep
+                     , bytestring
+                     , text
+                     , time
+  default-language:    Haskell2010
+
+test-suite data-ttn-tests
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  other-modules:       ParseSpec
+                       DownlinkSpec
+                       SpecHelper
+  build-depends:       base >= 4.7 && < 5
+                     , ttn
+                     , aeson
+                     , hspec
+                     , hspec-expectations
+                     , raw-strings-qq
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/sorki/data-ttn
