diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
 also experimental.
 
 - [API](#api)
-    - [`POST /v1/ping/:serviceId/:version`](#post-v1pingserviceidversion)
+    - [`POST /v1/ping`](#post-v1ping)
     - [`GET /v1/services/:serviceId`](#get-v1servicesserviceid)
     - [`GET /v1/services/:serviceId/:versionRange`](#get-v1servicesserviceidversionrange)
     - [`GET /v1/graph`](#get-v1graph)
@@ -21,18 +21,31 @@
 
 ## API
 
-### `POST /v1/ping/:serviceId/:version`
+### `POST /v1/ping`
 
-Register a service. Registration expires in 30 seconds.
+Register a service. The service name and version are obtained via the
+`User-Agent` header field. Registration expires in 30 seconds.
 
 - Request
     - Content-Type: [`application/vnd.legion-discovery.ping-request+json`](#applicationvndlegion-discoveryping-requestjson)
+    - Headers:
+        - `User-Agent`: The `User-Agent` request header is
+          mandatory, and must conform to the grammar specified in
+          [RFC-2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43)
+          and, further, must provide the product version that RFC-2616
+          describes as optional.
 - Response `204 No Content`: indicates a successful registration.
 
 ### `GET /v1/services/:serviceId`
 
 Returns all current instances of a service, regardless of version.
 
+- Request
+    - Headers:
+        - `User-Agent`: The `User-Agent` request header is
+          mandatory, and must conform to the grammar specified in
+          [RFC-2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43)
+
 - Response `200 Ok`
     - Content-Type: [`application/vnd.legion-discovery.instance-list+json`](#applicationvndlegion-discoveryinstance-listjson)
 
@@ -43,6 +56,12 @@
 version range syntax is the same as what cabal understands
 (e.g. `>= 0.1 && < 0.2`). You are really going to want to make sure and url
 encode this segment of the url.
+
+- Request
+    - Headers:
+        - `User-Agent`: The `User-Agent` request header is
+          mandatory, and must conform to the grammar specified in
+          [RFC-2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43)
 
 - Response `200 Ok`
     - Content-Type: [`application/vnd.legion-discovery.instance-list+json`](#applicationvndlegion-discoveryinstance-listjson)
diff --git a/legion-discovery.cabal b/legion-discovery.cabal
--- a/legion-discovery.cabal
+++ b/legion-discovery.cabal
@@ -1,5 +1,5 @@
 name:                legion-discovery
-version:             0.2.2.2
+version:             0.3.0.0
 synopsis:            A discovery service based on Legion.
 
 description:         A simple service discovery service based on Legion,
diff --git a/src/Network/Legion/Discovery.hs b/src/Network/Legion/Discovery.hs
--- a/src/Network/Legion/Discovery.hs
+++ b/src/Network/Legion/Discovery.hs
@@ -2,6 +2,7 @@
 {-# LANGUAGE NamedFieldPuns #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE TemplateHaskell #-}
 module Network.Legion.Discovery (
   main
 ) where
@@ -9,8 +10,11 @@
 import Canteven.HTTP (requestLogging, logExceptionsAndContinue,
   DecodeResult(Unsupported, BadEntity, Ok), FromEntity, decodeEntity)
 import Canteven.Log.MonadLog (getCantevenOutput)
-import Control.Monad (void)
+import Control.Exception (throwIO)
+import Control.Monad (void, (<=<))
 import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Logger (MonadLoggerIO, logInfo)
+import Control.Monad.Trans.Class (lift)
 import Data.Aeson (encode, object, (.=), FromJSON, parseJSON,
   Value(Object), (.:), eitherDecode)
 import Data.ByteString (ByteString, hGetContents)
@@ -25,7 +29,7 @@
 import Data.Text (pack, unpack)
 import Data.Text.Encoding (decodeUtf8, encodeUtf8)
 import Data.Time (getCurrentTime)
-import Data.Version (showVersion)
+import Data.Version (showVersion, Version)
 import Distribution.Text (simpleParse)
 import Distribution.Version (VersionRange, anyVersion)
 import Network.HTTP.Types (badRequest400, unsupportedMediaType415,
@@ -41,7 +45,7 @@
   unServiceId, State, Client(Client), cName, cVersion)
 import Network.Legion.Discovery.Config (servicePort, ekgPort)
 import Network.Legion.Discovery.Graphviz (toDotGraph)
-import Network.Legion.Discovery.HttpError (HttpError)
+import Network.Legion.Discovery.HttpError (HttpError(Critical, Transient))
 import Network.Legion.Discovery.LIO (runLIO, LIO)
 import Network.Legion.Discovery.UserAgent (UserAgent(UAProduct), Product(
   Product), productName, productVersion, parseUserAgent)
@@ -51,7 +55,8 @@
 import Web.Scotty.Format.Trans (respondTo, format, ResponseFormat)
 import Web.Scotty.Resource.Trans (resource, get, post)
 import Web.Scotty.Trans (scottyT, middleware, ScottyT, param, setHeader,
-  raw, status, text, ActionT, ScottyError, body, header)
+  raw, status, text, ActionT, ScottyError, body, header, defaultHandler,
+  raise)
 import qualified Data.ByteString.Lazy as LBS
 import qualified Data.Conduit.List as CL
 import qualified Data.Map as Map
@@ -75,28 +80,38 @@
         $ requestLogging logging
         . setServer "legion-discovery"
         . logExceptionsAndContinue logging
+      defaultHandler handleEx
       webService legion
 
 
+{- | Handle 'HttpError' errors. -}
+handleEx :: (MonadLoggerIO m, ScottyError e) => HttpError -> ActionT e m ()
+handleEx e@(Critical _) =
+  liftIO (throwIO e)
+handleEx (Transient statusCode msg) = do
+  lift . $(logInfo)
+    $ "Transient error: " <> msg
+  status statusCode
+  text (TL.fromStrict msg)
+
+
 {- | The web service endpoint definitions.  -}
 webService :: Runtime Input Output -> ScottyT HttpError LIO ()
 webService runtime = do
-  resource "/v1/ping/:serviceId/:version" $
-    post $
-      simpleParse <$> param "version" >>= \case
-        Nothing -> do
-          status badRequest400
-          text "Invalid version."
-        Just ver -> do
-          serviceId <- ServiceId <$> param "serviceId"
-          now <- Time <$> liftIO getCurrentTime
-          withEntity (\ PingRequest {serviceAddress} -> do
-              let req = Ping now serviceId ver serviceAddress
+  resource "/v1/ping" $
+    post . withStrictClients $ \clients -> do
+      now <- Time <$> liftIO getCurrentTime
+      withEntity (\ PingRequest {serviceAddress} -> do
+          sequence_ [
               makeRequest runtime (toKey serviceId) req >>= \case
-                PingResponse -> status noContent204
+                PingResponse -> return ()
                 InstanceList _ -> fail "Invalid runtime response."
                 ServiceResponse _ -> fail "Invalid runtime response."
-            )
+              | (serviceId, ver) <- clients
+              , let req = Ping now serviceId ver serviceAddress
+            ]
+          status noContent204
+        )
   resource "/v1/services" $
     get $ do
       now <- Time <$> liftIO getCurrentTime
@@ -169,6 +184,25 @@
           serviceId <- ServiceId <$> param "serviceId"
           queryResource serviceId range
   where
+    {- | Like 'withClients', but do not allow missing client versions. -}
+    withStrictClients :: (Monad m)
+      => ([(ServiceId, Version)] -> ActionT HttpError m ())
+      -> ActionT HttpError m ()
+    withStrictClients f =
+        withClients $ f <=< mapM toStrict
+      where
+        toStrict :: (Monad m)
+          => Client
+          -> ActionT HttpError m (ServiceId, Version)
+        toStrict Client {cName, cVersion = Just version} =
+          return (cName, version)
+        toStrict Client {cName, cVersion = Nothing} =
+          raise (Transient badRequest400 (
+              "Missing version for client product: "
+              <> unServiceId cName
+              <> ". Please fix your User-Agent header."
+            ))
+
     {- |
       Parse the list of clients from the User-Agent header and return
       the resulting 'ActionT', or else return an 'ActionT' that returns
diff --git a/src/Network/Legion/Discovery/HttpError.hs b/src/Network/Legion/Discovery/HttpError.hs
--- a/src/Network/Legion/Discovery/HttpError.hs
+++ b/src/Network/Legion/Discovery/HttpError.hs
@@ -3,9 +3,10 @@
   response types.
 -}
 module Network.Legion.Discovery.HttpError (
-  HttpError,
+  HttpError(..),
 ) where
 
+import Control.Exception (Exception)
 import Data.Text (Text, pack)
 import Data.Text.Lazy (fromStrict)
 import Network.HTTP.Types (Status)
@@ -14,6 +15,9 @@
 data HttpError
   = Transient Status Text
   | Critical Text
+  deriving (Show)
+
+instance Exception HttpError
 
 instance ScottyError HttpError where
   stringError = Critical . pack
