diff --git a/backend/model.go b/backend/model.go
index 1c53ba583a9e086746077383649cfd0b755e4681..e8947eaba4c0b89f3d9100da7ccc074ff8143cc0 100644
--- a/backend/model.go
+++ b/backend/model.go
@@ -3,7 +3,6 @@ package backend
 import (
 	"context"
 	"errors"
-	"fmt"
 	"os"
 	"strings"
 
@@ -30,6 +29,7 @@ type ldapConn interface {
 // all users.
 type backend struct {
 	conn                ldapConn
+	baseDN              string
 	userQuery           *queryConfig
 	userResourceQueries []*queryConfig
 	resources           *resourceRegistry
@@ -71,7 +71,8 @@ func newLDAPBackendWithConn(conn ldapConn, base string) (*backend, error) {
 	rsrc.register(accountserver.ResourceTypeDatabase, &databaseResourceHandler{baseDN: base})
 
 	return &backend{
-		conn: conn,
+		conn:   conn,
+		baseDN: base,
 		userQuery: mustCompileQueryConfig(&queryConfig{
 			Base:  "uid=${user},ou=People," + base,
 			Scope: "base",
@@ -186,9 +187,8 @@ func newUser(entry *ldap.Entry) (*accountserver.User, error) {
 	return user, nil
 }
 
-func getUserDN(user *accountserver.User) string {
-	// TODO: fix this
-	return fmt.Sprintf("uid=%s,ou=People,", user.Name)
+func (tx *backendTX) getUserDN(user *accountserver.User) string {
+	return joinDN("uid="+user.Name, "ou=People", tx.backend.baseDN)
 }
 
 // GetUser returns a user.
@@ -266,23 +266,23 @@ func (tx *backendTX) readAttributeValues(ctx context.Context, dn, attribute stri
 }
 
 func (tx *backendTX) SetUserPassword(ctx context.Context, user *accountserver.User, encryptedPassword string) error {
-	tx.setAttr(getUserDN(user), "userPassword", encryptedPassword)
+	tx.setAttr(tx.getUserDN(user), "userPassword", encryptedPassword)
 	return nil
 }
 
 func (tx *backendTX) GetUserEncryptionKeys(ctx context.Context, user *accountserver.User) ([]*accountserver.UserEncryptionKey, error) {
-	rawKeys := tx.readAttributeValues(ctx, getUserDN(user), "storageEncryptionKey")
+	rawKeys := tx.readAttributeValues(ctx, tx.getUserDN(user), "storageEncryptionKey")
 	return decodeUserEncryptionKeys(rawKeys), nil
 }
 
 func (tx *backendTX) SetUserEncryptionKeys(ctx context.Context, user *accountserver.User, keys []*accountserver.UserEncryptionKey) error {
 	encKeys := encodeUserEncryptionKeys(keys)
-	tx.setAttr(getUserDN(user), "storageEncryptionKey", encKeys...)
+	tx.setAttr(tx.getUserDN(user), "storageEncryptionKey", encKeys...)
 	return nil
 }
 
 func (tx *backendTX) SetUserEncryptionPublicKey(ctx context.Context, user *accountserver.User, pub []byte) error {
-	tx.setAttr(getUserDN(user), "storageEncryptionPublicKey", string(pub))
+	tx.setAttr(tx.getUserDN(user), "storageEncryptionPublicKey", string(pub))
 	return nil
 }
 
@@ -328,12 +328,12 @@ func (tx *backendTX) DeleteApplicationSpecificPassword(ctx context.Context, user
 }
 
 func (tx *backendTX) SetUserTOTPSecret(ctx context.Context, user *accountserver.User, secret string) error {
-	tx.setAttr(getUserDN(user), "totpSecret", secret)
+	tx.setAttr(tx.getUserDN(user), "totpSecret", secret)
 	return nil
 }
 
 func (tx *backendTX) DeleteUserTOTPSecret(ctx context.Context, user *accountserver.User) error {
-	tx.setAttr(getUserDN(user), "totpSecret")
+	tx.setAttr(tx.getUserDN(user), "totpSecret")
 	return nil
 }
 
diff --git a/integrationtest/integration_test.go b/integrationtest/integration_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..131021f034e35448acc5239c36810f11211b14d6
--- /dev/null
+++ b/integrationtest/integration_test.go
@@ -0,0 +1,194 @@
+package integrationtest
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+	"time"
+
+	"git.autistici.org/ai3/accountserver"
+	"git.autistici.org/ai3/accountserver/backend"
+	"git.autistici.org/ai3/accountserver/ldaptest"
+	"git.autistici.org/ai3/accountserver/server"
+	sso "git.autistici.org/id/go-sso"
+	"golang.org/x/crypto/ed25519"
+)
+
+const (
+	testLDAPPort = 42872
+	testLDAPAddr = "ldap://127.0.0.1:42872"
+
+	testSSODomain  = "domain"
+	testSSOService = "accountserver.domain/"
+	testAdminUser  = "admin"
+	testAdminGroup = "admins"
+)
+
+func withSSO(t testing.TB) (func(), sso.Signer, string) {
+	tmpf, err := ioutil.TempFile("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	pub, priv, err := ed25519.GenerateKey(nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	tmpf.Write(pub)
+	tmpf.Close()
+
+	signer, err := sso.NewSigner(priv)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return func() {
+		os.Remove(tmpf.Name())
+	}, signer, tmpf.Name()
+}
+
+type testClient struct {
+	srvURL string
+	signer sso.Signer
+}
+
+func (c *testClient) ssoTicket(username string) string {
+	var groups []string
+	if username == testAdminUser {
+		groups = append(groups, testAdminGroup)
+	}
+	signed, err := c.signer.Sign(sso.NewTicket(username, testSSOService, testSSODomain, "", groups, 1*time.Hour))
+	if err != nil {
+		panic(err)
+	}
+	return signed
+}
+
+func (c *testClient) request(uri string, req, out interface{}) error {
+	data, _ := json.Marshal(req)
+	resp, err := http.Post(c.srvURL+uri, "application/json", bytes.NewReader(data))
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != 200 {
+		return fmt.Errorf("http status code %d", resp.StatusCode)
+	}
+	if resp.Header.Get("Content-Type") != "application/json" {
+		return fmt.Errorf("unexpected content-type %s", resp.Header.Get("Content-Type"))
+	}
+	data, _ = ioutil.ReadAll(resp.Body)
+
+	log.Printf("response:\n%s\n", string(data))
+
+	if out == nil {
+		return nil
+	}
+	return json.Unmarshal(data, out)
+}
+
+func startService(t testing.TB) (func(), *testClient) {
+	stop := ldaptest.StartServer(t, &ldaptest.Config{
+		Dir:   "../ldaptest",
+		Port:  testLDAPPort,
+		Base:  "dc=example,dc=com",
+		LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"},
+	})
+
+	be, err := backend.NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
+	if err != nil {
+		t.Fatal("NewLDAPBackend", err)
+	}
+
+	ssoStop, signer, ssoPubKeyFile := withSSO(t)
+
+	var svcConfig accountserver.Config
+	svcConfig.SSO.PublicKeyFile = ssoPubKeyFile
+	svcConfig.SSO.Domain = testSSODomain
+	svcConfig.SSO.Service = testSSOService
+	svcConfig.SSO.AdminGroup = testAdminGroup
+	svcConfig.AvailableDomains = map[string][]string{
+		accountserver.ResourceTypeEmail: []string{"example.com"},
+	}
+
+	service, err := accountserver.NewAccountService(be, &svcConfig)
+	if err != nil {
+		stop()
+		t.Fatal("NewAccountService", err)
+	}
+
+	as := server.New(service, be)
+	srv := httptest.NewServer(as.Handler())
+
+	c := &testClient{
+		srvURL: srv.URL,
+		signer: signer,
+	}
+
+	return func() {
+		stop()
+		srv.Close()
+		ssoStop()
+	}, c
+}
+
+// Verify that authentication on GetUser works as expected:
+// - users can fetch their own data but not other users'
+// - admins can read everything.
+func TestIntegration_GetUser_Auth(t *testing.T) {
+	stop, c := startService(t)
+	defer stop()
+
+	testdata := []struct {
+		authUser   string
+		expectedOk bool
+	}{
+		{"uno@investici.org", true},
+		{"due@investici.org", false},
+		{testAdminUser, true},
+	}
+
+	for _, td := range testdata {
+		var user accountserver.User
+		err := c.request("/api/user/get", &accountserver.GetUserRequest{
+			RequestBase: accountserver.RequestBase{
+				Username: "uno@investici.org",
+				SSO:      c.ssoTicket(td.authUser),
+			},
+		}, &user)
+		if td.expectedOk && err != nil {
+			t.Errorf("access error for user %s: expected ok, got error: %v", td.authUser, err)
+		} else if !td.expectedOk && err == nil {
+			t.Errorf("access error for user %s: expected error, got ok", td.authUser)
+		}
+	}
+}
+
+// Verify that authentication on GetResource works as expected:
+// - users can fetch their own data but not other users'
+// - admins can read everything.
+func TestIntegration_ChangeUserPassword(t *testing.T) {
+	stop, c := startService(t)
+	defer stop()
+
+	err := c.request("/api/user/change_password", &accountserver.ChangeUserPasswordRequest{
+		PrivilegedRequestBase: accountserver.PrivilegedRequestBase{
+			RequestBase: accountserver.RequestBase{
+				Username: "uno@investici.org",
+				SSO:      c.ssoTicket("uno@investici.org"),
+			},
+			CurPassword: "password",
+		},
+		Password: "new_password",
+	}, nil)
+
+	if err != nil {
+		t.Fatal("ChangePassword", err)
+	}
+}
diff --git a/integrationtest/testdata/base.ldif b/integrationtest/testdata/base.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..fcbc2849fa34661c06ccd104a91052e2fa326dde
--- /dev/null
+++ b/integrationtest/testdata/base.ldif
@@ -0,0 +1,16 @@
+
+dn: dc=example,dc=com
+objectclass: domain
+objectclass: top
+dc: example
+
+dn: ou=People,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: People
+
+dn: ou=Lists,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: Lists
+
diff --git a/integrationtest/testdata/test1.ldif b/integrationtest/testdata/test1.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..9bf6880683b247b165f7b48ccd62866470c24af8
--- /dev/null
+++ b/integrationtest/testdata/test1.ldif
@@ -0,0 +1,106 @@
+dn: uid=uno@investici.org,ou=People,dc=example,dc=com
+cn: uno@investici.org
+objectClass: top
+objectClass: person
+objectClass: posixAccount
+objectClass: shadowAccount
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+loginShell: /bin/false
+uidNumber: 19475
+shadowMax: 99999
+gidNumber: 2000
+gecos: uno@investici.org
+sn: Private
+homeDirectory: /var/empty
+uid: uno@investici.org
+givenName: Private
+shadowLastChange: 12345
+shadowWarning: 7
+preferredLanguage: it
+userPassword:: e2NyeXB0fSQ2JG1wVzdTZHZROG52OFJabE8kcFNqbm1hLi9CZDNIWU1hL29sMGZ
+ FRmZrS3pWRjAxUkkzMnpISEoxNnc2V2xaajFuSFVzMmd4ZXZIVDdoemdELnJ5Lk1BZWM3REptZVZ5
+ WEtkeDV0QTE=
+
+dn: mail=uno@investici.org,uid=uno@investici.org,ou=People,dc=example,dc=com
+status: active
+recoverQuestion:: dGkgc2VpIG1haSDDuMOgdHRvIG1hbGUgY2FkZW5kbyBkYSB1biBwYWxhenpv
+ IGRpIG90dG8gcGlhbmk/
+objectClass: top
+objectClass: virtualMailUser
+userPassword:: e2NyeXB0fSQ2JG1wVzdTZHZROG52OFJabE8kcFNqbm1hLi9CZDNIWU1hL29sMGZ
+ FRmZrS3pWRjAxUkkzMnpISEoxNnc2V2xaajFuSFVzMmd4ZXZIVDdoemdELnJ5Lk1BZWM3REptZVZ5
+ WEtkeDV0QTE=
+uidNumber: 19475
+host: host2
+mailAlternateAddress: uno@anche.no
+recoverAnswer: {crypt}$1$wtEa4TKB$lxeyenkQ1yfxECn7WVQQ0/
+gidNumber: 2000
+mail: uno@investici.org
+creationDate: 2002-05-07
+mailMessageStore: investici.org/uno/
+originalHost: host2
+
+dn: ftpname=uno,uid=uno@investici.org,ou=People,dc=example,dc=com
+status: active
+givenName: Private
+cn: uno
+objectClass: top
+objectClass: person
+objectClass: posixAccount
+objectClass: shadowAccount
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+objectClass: ftpAccount
+loginShell: /bin/false
+shadowWarning: 7
+uidNumber: 19475
+host: host2
+shadowMax: 99999
+ftpname: uno
+gidNumber: 33
+gecos: FTP Account for uno@investici.org
+sn: Private
+homeDirectory: /home/users/investici.org/uno
+uid: uno
+creationDate: 01-08-2013
+shadowLastChange: 12345
+originalHost: host2
+userPassword:: e2NyeXB0fSQ2JElDYkx1WTI3QWl6bC5FeEgkUDhOZHJ3VEtxZ2UwQUp3QW9oNE1
+ EYlUxU3EySGtuRkF1cEx2RUI0U28waEw5NWtpZ3dIeXQuQnYxS0J5SFM2MXd6RnZuLnJsMEN4eFpx
+ RVgzUnVxbDE=
+
+dn: alias=uno,uid=uno@investici.org,ou=People,dc=example,dc=com
+status: active
+parentSite: autistici.org
+objectClass: top
+objectClass: subSite
+alias: uno
+host: host2
+documentRoot: /home/users/investici.org/uno/html-uno
+creationDate: 01-08-2013
+originalHost: host2
+statsId: 2191
+
+dn: cn=example.com,uid=uno@investici.org,ou=People,dc=example,dc=com
+status: active
+acceptMail: true
+objectClass: top
+objectClass: virtualHost
+cn: example.com
+host: host2
+documentRoot: /home/users/investici.org/uno/html-example.com
+creationDate: 02-08-2013
+originalHost: host2
+statsId: 2192
+
+dn: dbname=unodb,alias=uno,uid=uno@investici.org,ou=People,dc=example,dc=com
+status: active
+clearPassword: password
+objectClass: top
+objectClass: dbMysql
+dbname: unodb
+dbuser: unodb
+host: host2
+creationDate: 01-08-2013
+originalHost: host2
diff --git a/server/server.go b/server/server.go
index 0d84166800d70e10db72e176b5d2b99dcd7b4fee..eff6995eed45fc1cf99b7d0550a86e7ea4e6e187 100644
--- a/server/server.go
+++ b/server/server.go
@@ -10,7 +10,7 @@ import (
 	as "git.autistici.org/ai3/accountserver"
 )
 
-type txHandler func(as.TX, http.ResponseWriter, *http.Request) error
+type txHandler func(as.TX, http.ResponseWriter, *http.Request) (interface{}, error)
 
 var errBadRequest = errors.New("bad request")
 
@@ -26,7 +26,7 @@ func New(service *as.AccountService, backend as.Backend) *AccountServer {
 	}
 }
 
-var emptyResponse = map[string]string{}
+var emptyResponse struct{}
 
 func errToStatus(err error) int {
 	switch {
@@ -41,168 +41,158 @@ func errToStatus(err error) int {
 	}
 }
 
-func (s *AccountServer) handleGetUser(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleGetUser(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.GetUserRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	user, err := s.service.GetUser(r.Context(), tx, &req)
 	if err != nil {
 		log.Printf("GetUser(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, user)
-	return nil
+	return user, nil
 }
 
-func (s *AccountServer) handleChangeUserPassword(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleChangeUserPassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.ChangeUserPasswordRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	if err := s.service.ChangeUserPassword(r.Context(), tx, &req); err != nil {
 		log.Printf("ChangeUserPassword(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, emptyResponse)
-	return nil
+	return &emptyResponse, nil
 }
 
-func (s *AccountServer) handleCreateApplicationSpecificPassword(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleCreateApplicationSpecificPassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.CreateApplicationSpecificPasswordRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	resp, err := s.service.CreateApplicationSpecificPassword(r.Context(), tx, &req)
 	if err != nil {
 		log.Printf("CreateApplicationSpecificPassword(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, resp)
-	return nil
+	return resp, nil
 }
 
-func (s *AccountServer) handleDeleteApplicationSpecificPassword(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleDeleteApplicationSpecificPassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.DeleteApplicationSpecificPasswordRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	if err := s.service.DeleteApplicationSpecificPassword(r.Context(), tx, &req); err != nil {
 		log.Printf("DeleteApplicationSpecificPassword(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, emptyResponse)
-	return nil
+	return &emptyResponse, nil
 }
 
-func (s *AccountServer) handleEnableResource(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleEnableResource(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.EnableResourceRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	if err := s.service.EnableResource(r.Context(), tx, &req); err != nil {
 		log.Printf("EnableResource(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, emptyResponse)
-	return nil
+	return &emptyResponse, nil
 }
 
-func (s *AccountServer) handleDisableResource(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleDisableResource(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.DisableResourceRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	if err := s.service.DisableResource(r.Context(), tx, &req); err != nil {
 		log.Printf("DisableResource(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, emptyResponse)
-	return nil
+	return &emptyResponse, nil
 }
 
-func (s *AccountServer) handleChangeResourcePassword(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleChangeResourcePassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.ChangeResourcePasswordRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	if err := s.service.ChangeResourcePassword(r.Context(), tx, &req); err != nil {
 		log.Printf("ChangeResourcePassword(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, emptyResponse)
-	return nil
+	return &emptyResponse, nil
 }
 
-func (s *AccountServer) handleMoveResource(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleMoveResource(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.MoveResourceRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	resp, err := s.service.MoveResource(r.Context(), tx, &req)
 	if err != nil {
 		log.Printf("MoveResource(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, resp)
-	return nil
+	return resp, nil
 }
 
-func (s *AccountServer) handleEnableOTP(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleEnableOTP(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.EnableOTPRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	resp, err := s.service.EnableOTP(r.Context(), tx, &req)
 	if err != nil {
 		log.Printf("EnableOTP(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, resp)
-	return nil
+	return resp, nil
 }
 
-func (s *AccountServer) handleDisableOTP(tx as.TX, w http.ResponseWriter, r *http.Request) error {
+func (s *AccountServer) handleDisableOTP(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
 	var req as.DisableOTPRequest
 	if !serverutil.DecodeJSONRequest(w, r, &req) {
-		return errBadRequest
+		return nil, errBadRequest
 	}
 
 	if err := s.service.DisableOTP(r.Context(), tx, &req); err != nil {
 		log.Printf("DisableOTP(%s): error: %v", req.Username, err)
 		http.Error(w, err.Error(), errToStatus(err))
-		return err
+		return nil, err
 	}
 
-	serverutil.EncodeJSONResponse(w, emptyResponse)
-	return nil
+	return &emptyResponse, nil
 }
 
 func (s *AccountServer) withTx(f txHandler) http.HandlerFunc {
@@ -214,15 +204,19 @@ func (s *AccountServer) withTx(f txHandler) http.HandlerFunc {
 			return
 		}
 
-		if err := f(tx, w, r); err == nil {
-			// Automatically commit the transaction (if
-			// the handler didn't do this itself).
-			if err := tx.Commit(r.Context()); err != nil {
-				log.Printf("Commit error: %v", err)
-				http.Error(w, err.Error(), http.StatusInternalServerError)
-				return
-			}
+		resp, err := f(tx, w, r)
+		if err != nil {
+			return
+		}
+		// Automatically commit the transaction (if
+		// the handler didn't do this itself).
+		if err := tx.Commit(r.Context()); err != nil {
+			log.Printf("Commit error: %v", err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
 		}
+
+		serverutil.EncodeJSONResponse(w, resp)
 	}
 }
 
diff --git a/types.go b/types.go
index a544ed9dceda9a70485afa4f9f847d3af497eba4..3aea54d9cb7a7260fee2bb0915acc45473599907 100644
--- a/types.go
+++ b/types.go
@@ -161,7 +161,9 @@ func (i *ResourceID) UnmarshalJSON(data []byte) error {
 	if err != nil {
 		return err
 	}
-	*i, err = ParseResourceID(s)
+	if s != "" {
+		*i, err = ParseResourceID(s)
+	}
 	return err
 }