diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2017 Elliot Cameron and Ziptastic and Grafted-In LLC
+
+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 Elliot Cameron 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+# ziptastic-core
+
+[![Haskell Programming Language](https://img.shields.io/badge/language-Haskell-blue.svg)](http://www.haskell.org)
+[![BSD3 License](http://img.shields.io/badge/license-BSD3-brightgreen.svg)](https://tldrlegal.com/license/bsd-3-clause-license-%28revised%29)
+
+Core [Servant](http://haskell-servant.readthedocs.io/en/stable/) specification for the [Ziptastic](https://www.getziptastic.com/) API providing forward and reverse geocoding via country, zip code (postal code), latitude, and longitude.
+
+This package is maintained by [Grafted-In](https://www.graftedin.io/).
+
+If you want to use the Ziptastic API in your application, try the [ziptastic-client](http://hackage.haskell.org/package/ziptastic-client) package.
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/Ziptastic/Core.hs b/src/Ziptastic/Core.hs
new file mode 100644
--- /dev/null
+++ b/src/Ziptastic/Core.hs
@@ -0,0 +1,138 @@
+{-# LANGUAGE
+    DataKinds
+  , DeriveGeneric
+  , FlexibleInstances
+  , GeneralizedNewtypeDeriving
+  , OverloadedStrings
+  , TypeOperators
+#-}
+
+-- | This module provides a complete and type-safe API specification for
+-- Ziptastic's forward and reverse geocoding API using Servant
+-- (<https://www.getziptastic.com/>).
+--
+-- To use this specification in your application, try the @ziptastic-client@ package.
+module Ziptastic.Core
+  ( Api
+  , ApiKey(..)
+  , ForApi(..)
+  , LocaleCoords(..)
+  , LocaleInfo(..)
+  , baseUrlHost
+  , baseUrlIsHttps
+  , baseUrlPath
+  , baseUrlPort
+  ) where
+
+import           Control.Monad (when)
+import           Data.ISO3166_CountryCodes (CountryCode)
+import qualified Data.Aeson as Json
+import qualified Data.Aeson.Types as Json
+import           Data.Aeson ((.:), (.:?))
+import           Data.String (IsString)
+import           Data.Text (Text)
+import qualified Data.Text as T
+import           Data.Text.Encoding (encodeUtf8)
+import           Data.Time.Zones.All (TZLabel, fromTZName)
+import           GHC.Generics (Generic)
+import           Servant.API (Capture, Get, Header, JSON, (:>), (:<|>))
+import           Text.Read (readMaybe)
+import           Web.HttpApiData (ToHttpApiData, toUrlPiece)
+
+
+data LocaleCoords = LocaleCoords
+  { coordsLatitude  :: Double
+  , coordsLongitude :: Double
+  , coordsGeohash   :: Text    -- ^ Will never be empty.
+  } deriving (Eq, Generic, Show)
+
+data LocaleInfo = LocaleInfo
+  { localeCity         :: Maybe Text  -- ^ If given, text will never be empty.
+  , localeCoords       :: Maybe LocaleCoords
+  , localeCountry      :: CountryCode
+  , localeCounty       :: Maybe Text -- ^ If given, text will never be empty.
+  , localeRegionFull   :: Maybe Text -- ^ If given, text will never be empty.
+  , localeRegionAbbrev :: Maybe Text -- ^ If given, text will never be empty.
+  , localePostalCode   :: Text       -- ^ Will never be empty.
+  , localeTimeZone     :: Maybe TZLabel
+  } deriving (Eq, Generic, Show)
+
+instance Json.FromJSON LocaleInfo where
+  parseJSON (Json.Object v) = do
+    maybeLat     <- v .:? "latitude"
+    maybeLong    <- v .:? "longitude"
+    maybeGeohash <- v `optionalStr` "geohash"
+
+    countryCodeStr <- v .: "country"
+    maybeTzText <- v `optionalStr` "timezone"
+
+    postalCode <- v .: "postal_code"
+    when (T.null postalCode) $ fail "Invalid postal code"
+
+    let
+      coords = case (maybeLat, maybeLong, maybeGeohash) of
+        (Just lat, Just long, Just geohash) -> Just (LocaleCoords lat long geohash)
+        _ -> Nothing
+
+      tzParser :: Text -> Json.Parser TZLabel
+      tzParser tz = maybe (fail $ "Unrecognized time zone: " ++ T.unpack tz) pure (fromTZName $ encodeUtf8 tz)
+
+    LocaleInfo
+      <$> v `optionalStr` "city"
+      <*> pure coords
+      <*> maybe (fail $ "Unrecognized country code: " ++ countryCodeStr) pure (readMaybe countryCodeStr)
+      <*> v `optionalStr` "county"
+      <*> v `optionalStr` "state"
+      <*> v `optionalStr` "state_short"
+      <*> pure postalCode
+      <*> maybe (pure Nothing) (fmap Just . tzParser) maybeTzText
+
+    where
+      optionalStr v' key = do
+        val <- v' .:? key
+        pure $ case val of
+          Just x -> if T.null x then Nothing else Just x
+          Nothing -> Nothing
+
+  parseJSON x = Json.typeMismatch "LocaleInfo" x
+
+newtype ApiKey = ApiKey { getApiKey :: Text } deriving (Eq, Generic, IsString, Show, ToHttpApiData)
+
+-- | A generic wrapper for giving external data types instances for our uses.
+newtype ForApi a = ForApi a
+
+instance ToHttpApiData (ForApi CountryCode) where
+  toUrlPiece (ForApi countryCode) = T.pack (show countryCode)
+
+type Api = "v3" :> Header "x-key" ApiKey :> ApiEndpoints
+type ApiEndpoints = ForwardGeocodingApi :<|> ReverseGeocodingApi
+
+type LocaleInfoResponse = Get '[JSON] [LocaleInfo]
+
+type ForwardGeocodingApi
+  =  Capture "country-code" (ForApi CountryCode)
+  :> Capture "postal-code" Text
+  :> LocaleInfoResponse
+
+type ReverseGeocodingApi
+  =  "reverse"
+  :> Capture "latitude"  Double
+  :> Capture "longitude" Double
+  :>
+    (    Capture "radius-in-meters" Int :> LocaleInfoResponse
+    :<|> LocaleInfoResponse  -- use default radius
+    )
+
+
+-- API URL components
+baseUrlIsHttps :: Bool
+baseUrlIsHttps = True
+
+baseUrlHost :: String
+baseUrlHost = "zip.getziptastic.com"
+
+baseUrlPort :: Int
+baseUrlPort = 443
+
+baseUrlPath :: String
+baseUrlPath = ""
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,90 @@
+{-# LANGUAGE
+    OverloadedStrings
+  , QuasiQuotes
+#-}
+
+module Main (main) where
+
+import           Control.Monad
+import           Data.Aeson (decodeStrict')
+import qualified Data.ISO3166_CountryCodes as CC
+import           Data.Monoid
+import           Data.Text (Text)
+import           Data.Text.Encoding (encodeUtf8)
+import           Test.Hspec
+import           Data.String.Here (here, iTrim)
+import           Data.Time.Zones.All as TZ
+
+import           Ziptastic.Core
+
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec = describe "JSON decoding LocaleInfo" $ do
+  let parseTemplate :: Maybe (Text, Text) -> Maybe LocaleInfo
+      parseTemplate = decodeStrict' . encodeUtf8 . exampleJsonTemplate
+
+  it "accepts the standard example" $
+     parseTemplate Nothing `shouldBe` Just exampleLocaleInfo
+
+  it "nullifies coordinates if any related fields are null" $
+    forM_ [ ("geohash", quoted ""), ("geohash", "null")
+          , ("latitude", "null"), ("longitude", "null") ] $ \(field, val) ->
+      parseTemplate (Just (field, val)) `shouldBe` Just exampleLocaleInfo{localeCoords = Nothing}
+
+  it "accepts empty time zone" $
+    forM_ [quoted "", "null"] $ \val ->
+      parseTemplate (Just ("timezone", val))
+        `shouldBe` Just exampleLocaleInfo{localeTimeZone = Nothing}
+
+  it "does not accept invalid time zone" $
+    parseTemplate (Just ("timezone", "blah")) `shouldBe` Nothing
+
+  it "does not accept invalid country code" $
+    parseTemplate (Just ("country", "zz")) `shouldBe` Nothing
+
+  it "does not accept empty postal code" $
+    parseTemplate (Just ("postal_code", quoted "")) `shouldBe` Nothing
+
+
+exampleJsonTemplate :: Maybe (Text, Text) -> Text
+exampleJsonTemplate fieldOverride = [iTrim|
+    {
+      ${f "city" $ quoted "Owosso"},
+      ${f "geohash" $ quoted "dpshsfsytw8k"},
+      ${f "country" $ quoted "US"},
+      ${f "county" $ quoted "Shiawassee"},
+      ${f "state" $ quoted "Michigan"},
+      ${f "state_short" $ quoted "MI"},
+      ${f "postal_code" $ quoted "48867"},
+      ${f "latitude" "42.9934"},
+      ${f "longitude" "-84.1595"},
+      ${f "timezone" $ quoted "America/Detroit"}
+    }
+  |] :: Text
+  where
+    f :: Text -> Text -> Text
+    f field defaultVal = quoted field <> ": " <> case fieldOverride of
+      Nothing -> defaultVal
+      Just (key, val) -> if field == key then val else defaultVal
+
+quoted :: Text -> Text
+quoted x = "\"" <> x <> "\""
+
+exampleLocaleInfo :: LocaleInfo
+exampleLocaleInfo = LocaleInfo
+  { localeCity         = Just "Owosso"
+  , localeCountry      = CC.US
+  , localeCounty       = Just "Shiawassee"
+  , localeRegionFull   = Just "Michigan"
+  , localeRegionAbbrev = Just "MI"
+  , localePostalCode   = "48867"
+  , localeCoords       = Just LocaleCoords
+      { coordsLatitude  = 42.9934
+      , coordsLongitude = (-84.1595)
+      , coordsGeohash   = "dpshsfsytw8k"
+      }
+  , localeTimeZone   = Just TZ.America__Detroit
+  }
diff --git a/ziptastic-core.cabal b/ziptastic-core.cabal
new file mode 100644
--- /dev/null
+++ b/ziptastic-core.cabal
@@ -0,0 +1,65 @@
+name:                ziptastic-core
+version:             0.1.0.0
+synopsis:
+  Core Servant specification for the Ziptastic API (https://www.getziptastic.com) for doing forward and reverse geocoding.
+description:
+  This package provides a type-safe Servant specification for the Ziptastic
+  (https://www.getziptastic.com) API for doing forward and reverse geocoding
+  via zip/postal code, latitude, and longitude.
+  .
+  If you want to use the Ziptastic API in your application, try the ziptastic-client package.
+  .
+  This package is maintained by Grafted-In (https://www.graftedin.io/).
+homepage:            https://github.com/Ziptastic/ziptastic-haskell#readme
+license:             BSD3
+license-file:        LICENSE
+author:              Elliot Cameron
+maintainer:          elliot@graftedin.io
+copyright:           2017 Elliot Cameron and Ziptastic and Grafted-In LLC
+category:            Web
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+tested-with:         GHC==8.0.2
+
+library
+  hs-source-dirs:  src
+  exposed-modules: Ziptastic.Core
+  build-depends:
+      aeson
+    , base >= 4.7 && < 5
+    , bytestring
+    , http-api-data
+    , iso3166-country-codes
+    , servant
+    , text
+    , tz
+  default-language: Haskell2010
+  other-extensions:
+    DataKinds
+    DeriveGeneric
+    FlexibleInstances
+    GeneralizedNewtypeDeriving
+    OverloadedStrings
+    TypeOperators
+  ghc-options: -Wall
+
+test-suite test-core
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  build-depends:
+      aeson
+    , base
+    , here
+    , hspec
+    , iso3166-country-codes
+    , text
+    , tz
+    , ziptastic-core
+  ghc-options:      -threaded -rtsopts -with-rtsopts=-N
+  default-language: Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/Ziptastic/ziptastic-haskell
