diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,17 @@
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,6 @@
+module Main(main)
+
+import Distribution.Simple
+
+main :: IO ()
+main = defaultMain
diff --git a/api-opentheory-unicode.cabal b/api-opentheory-unicode.cabal
new file mode 100644
--- /dev/null
+++ b/api-opentheory-unicode.cabal
@@ -0,0 +1,39 @@
+name: api-opentheory-unicode
+version: 1.0
+category: Text
+synopsis: OpenTheory unicode character API
+license: MIT
+license-file: LICENSE
+cabal-version: >= 1.8.0.2
+build-type: Simple
+author: Joe Leslie-Hurd <joe@gilith.com>
+maintainer: Joe Leslie-Hurd <joe@gilith.com>
+description:
+  This package provides an API to the unicode character package
+  opentheory-unicode which is automatically generated by OpenTheory.
+
+Library
+  build-depends:
+    base >= 4.0 && < 5.0,
+    bytestring >= 0.9 && < 1.0,
+    opentheory-unicode >= 1.0 && < 2.0
+
+  hs-source-dirs: src
+
+  ghc-options: -Wall
+
+  exposed-modules:
+    Unicode
+
+executable api-opentheory-unicode-test
+  build-depends:
+    base >= 4.0 && < 5.0,
+    bytestring >= 0.9 && < 1.0,
+    directory >= 1.0 && < 2.0,
+    opentheory-unicode >= 1.0 && < 2.0
+
+  hs-source-dirs: src, testsrc
+
+  ghc-options: -Wall
+
+  main-is: Main.hs
diff --git a/src/Unicode.hs b/src/Unicode.hs
new file mode 100644
--- /dev/null
+++ b/src/Unicode.hs
@@ -0,0 +1,81 @@
+{- |
+module: Unicode
+description: Unicode character API
+license: MIT
+
+maintainer: Joe Leslie-Hurd <joe@gilith.com>
+stability: provisional
+portability: portable
+-}
+module Unicode
+  ( toString,
+    newline,
+    decode,
+    encode,
+    reencode,
+    hDecode,
+    hEncode,
+    hReencode,
+    decodeFile,
+    encodeFile,
+    reencodeFile )
+where
+
+import qualified Data.ByteString.Lazy as ByteString
+import qualified Data.Char as Char
+import qualified Data.Word as Word
+import qualified System.IO as IO
+
+import qualified OpenTheory.Unicode.UTF8 as UTF8
+import OpenTheory.Unicode
+
+toString :: Unicode -> String
+toString = show . Char.chr . fromIntegral . unUnicode
+
+newline :: [Unicode]
+newline = [Unicode 10]
+
+decode :: IO [Either Word.Word8 Unicode]
+decode =
+    do b <- ByteString.getContents
+       return (UTF8.decode (ByteString.unpack b))
+
+encode :: [Unicode] -> IO ()
+encode c =
+    let b = ByteString.pack (UTF8.encode c) in
+    ByteString.putStr b
+
+reencode :: [Either Word.Word8 Unicode] -> IO ()
+reencode c =
+    let b = ByteString.pack (UTF8.reencode c) in
+    ByteString.putStr b
+
+hDecode :: IO.Handle -> IO [Either Word.Word8 Unicode]
+hDecode h =
+    do b <- ByteString.hGetContents h
+       return (UTF8.decode (ByteString.unpack b))
+
+hEncode :: IO.Handle -> [Unicode] -> IO ()
+hEncode h c =
+    let b = ByteString.pack (UTF8.encode c) in
+    ByteString.hPut h b
+
+hReencode :: IO.Handle -> [Either Word.Word8 Unicode] -> IO ()
+hReencode h c =
+    let b = ByteString.pack (UTF8.reencode c) in
+    ByteString.hPut h b
+
+decodeFile :: FilePath -> IO [Either Word.Word8 Unicode]
+decodeFile f =
+    do b <- ByteString.readFile f
+       return (UTF8.decode (ByteString.unpack b))
+
+encodeFile :: FilePath -> [Unicode] -> IO ()
+encodeFile f c =
+    let b = ByteString.pack (UTF8.encode c) in
+    ByteString.writeFile f b
+
+reencodeFile :: FilePath -> [Either Word.Word8 Unicode] -> IO ()
+reencodeFile f c =
+    let b = ByteString.pack (UTF8.reencode c) in
+    ByteString.writeFile f b
diff --git a/testsrc/Main.hs b/testsrc/Main.hs
new file mode 100644
--- /dev/null
+++ b/testsrc/Main.hs
@@ -0,0 +1,109 @@
+{- |
+module: Main
+description: Testing the Unicode character library and API
+license: MIT
+
+maintainer: Joe Leslie-Hurd <joe@gilith.com>
+stability: provisional
+portability: portable
+-}
+module Main
+  ( main )
+where
+
+import qualified Data.List as List
+import qualified Data.Either as Either
+import qualified Data.Word as Word
+import qualified System.Directory as Directory
+
+import Unicode
+import qualified OpenTheory.Unicode as Unicode
+
+demoLength :: Int
+demoLength = 7621
+
+testLength :: Int
+testLength = 79
+
+noInvalid :: [Either Word.Word8 Unicode.Unicode] -> Bool
+noInvalid = all (Either.either (const False) (const True))
+
+getTestFiles :: FilePath -> IO [FilePath]
+getTestFiles d =
+    do fs <- Directory.getDirectoryContents d
+       let fs' = filter (List.isPrefixOf "test") fs
+       return (map (\f -> d ++ "/" ++ f) fs')
+
+readTestCharFile :: Maybe Int -> FilePath -> IO ()
+readTestCharFile x f =
+    do l <- decodeFile f
+       let a = noInvalid l
+       case x of
+         Nothing ->
+             if a
+               then error $ "invalid file " ++ f ++ " was successfully decoded"
+               else return ()
+         Just n ->
+             if not a
+               then error $ "valid file " ++ f ++ " could not be decoded"
+               else
+                 if length l == n
+                   then return ()
+                   else
+                     error $ "valid file " ++ f ++ " has bad line length: " ++
+                             show (length l)
+
+readValidCharFile :: Int -> FilePath -> IO ()
+readValidCharFile n = readTestCharFile (Just n)
+
+readInvalidCharFile :: FilePath -> IO ()
+readInvalidCharFile = readTestCharFile Nothing
+
+partitionTestCharFile :: IO ()
+partitionTestCharFile =
+    do skip <- Directory.doesFileExist "test/valid/test061.txt"
+       if skip
+         then return ()
+         else
+           do putStrLn "\nsplitting UTF-8 test file into valid and invalid lines"
+              cs <- decodeFile "test/test.txt"
+              mapM_ outputLine (filter testLine (readLines 1 cs))
+  where
+    isNewline :: Either Word.Word8 Unicode.Unicode -> Bool
+    isNewline (Left _) = False
+    isNewline (Right c) = let n = Unicode.unUnicode c in n == 10 || n == 13
+
+    chopNewline :: [Either Word.Word8 Unicode.Unicode] ->
+                   [Either Word.Word8 Unicode.Unicode]
+    chopNewline [] = []
+    chopNewline (_ : cs) = cs
+
+    readLines :: Int -> [Either Word.Word8 Unicode.Unicode] ->
+                 [(Int,[Either Word.Word8 Unicode.Unicode])]
+    readLines _ [] = []
+    readLines lineno inp =
+        let (line,inp') = List.span (not . isNewline) inp in
+        let inp'' = chopNewline inp' in
+        (lineno,line) : readLines (lineno + 1) inp''
+
+    testLine :: (Int,[Either Word.Word8 Unicode.Unicode]) -> Bool
+    testLine (lineno,line) = not (lineno < 61 || null line)
+
+    outputLine :: (Int,[Either Word.Word8 Unicode.Unicode]) -> IO ()
+    outputLine (lineno,line) =
+        let valid = noInvalid line in
+        let n = show lineno in
+        let f = "test/" ++ (if valid then "valid" else "invalid") ++
+                "/test" ++ replicate (3 - length n) '0' ++ n ++ ".txt" in
+        reencodeFile f line
+
+main :: IO ()
+main =
+    do partitionTestCharFile
+       readValidCharFile demoLength "test/demo.txt"
+       validTestFiles <- getTestFiles "test/valid"
+       invalidTestFiles <- getTestFiles "test/invalid"
+       mapM_ (readValidCharFile testLength) validTestFiles
+       mapM_ readInvalidCharFile invalidTestFiles
+       putStrLn "  all tests pass"
+       return ()
