diff --git a/pkg/api/api.go b/pkg/api/api.go
index 4e22860..9910d9f 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -34,30 +34,36 @@ func (a *api) Subrouter() http.Handler {
 	return r
 }
 
-var statusMapping = map[error]int{
-	talkgroups.ErrNotFound: http.StatusNotFound,
-	pgx.ErrNoRows:          http.StatusNotFound,
+type errResponse struct {
+	text string
+	code int
 }
 
-func httpCode(err error) int {
+var statusMapping = map[error]errResponse{
+	talkgroups.ErrNotFound: {talkgroups.ErrNotFound.Error(), http.StatusNotFound},
+	pgx.ErrNoRows:          {"no such record", http.StatusNotFound},
+}
+
+func httpCode(err error) (string, int) {
 	c, ok := statusMapping[err]
 	if ok {
-		return c
+		return c.text, c.code
 	}
 
 	for e, c := range statusMapping { // check if err wraps an error we know about
 		if errors.Is(err, e) {
-			return c
+			return c.text, c.code
 		}
 	}
 
-	return http.StatusInternalServerError
+	return err.Error(), http.StatusInternalServerError
 }
 
 func writeResponse(w http.ResponseWriter, r *http.Request, data interface{}, err error) {
 	if err != nil {
 		log.Error().Str("path", r.URL.Path).Err(err).Msg("request failed")
-		http.Error(w, err.Error(), httpCode(err))
+		text, code := httpCode(err)
+		http.Error(w, text, code)
 		return
 	}
 
@@ -66,7 +72,8 @@ func writeResponse(w http.ResponseWriter, r *http.Request, data interface{}, err
 	err = enc.Encode(data)
 	if err != nil {
 		log.Error().Str("path", r.URL.Path).Err(err).Msg("response marshal failed")
-		http.Error(w, err.Error(), httpCode(err))
+		text, code := httpCode(err)
+		http.Error(w, text, code)
 		return
 	}
 }
diff --git a/pkg/api/talkgroups.go b/pkg/api/talkgroups.go
index e16f8f3..e699b78 100644
--- a/pkg/api/talkgroups.go
+++ b/pkg/api/talkgroups.go
@@ -1,10 +1,11 @@
 package api
 
 import (
-	"fmt"
+	"encoding/json"
 	"net/http"
 
 	"dynatron.me/x/stillbox/internal/forms"
+	"dynatron.me/x/stillbox/pkg/database"
 	"dynatron.me/x/stillbox/pkg/talkgroups"
 
 	"github.com/go-chi/chi/v5"
@@ -80,37 +81,27 @@ func (tga *talkgroupAPI) putTalkgroup(w http.ResponseWriter, r *http.Request) {
 		badReq(w, err)
 		return
 	}
-	/*
-		ctx := r.Context()
-		tgs := talkgroups.StoreFrom(ctx)
 
-		tg, err := tgs.TG(ctx, id.ToID())
-		switch err {
-		case nil:
-		case talkgroups.ErrNotFound:
-			reqErr(w, err, http.StatusNotFound)
-			return
-		default:
-			reqErr(w, err, http.StatusInternalServerError)
-		}
-	*/
+	ctx := r.Context()
+	tgs := talkgroups.StoreFrom(ctx)
 
-	input := struct {
-		Name        *string  `form:"name"`
-		AlphaTag    *string  `form:"alpha_tag"`
-		TgGroup     *string  `form:"tg_group"`
-		Frequency   *int32   `form:"frequency"`
-		Metadata    []byte   `form:"metadata"`
-		Tags        []string `form:"tags"`
-		Alert       *bool    `form:"alert"`
-		AlertConfig []byte   `form:"alert_config"`
-		Weight      *float32 `form:"weight"`
-	}{}
+	input := database.UpdateTalkgroupParams{}
 
-	err = forms.Unmarshal(r, &input, forms.WithAcceptBlank(), forms.WithOmitEmpty())
+	err = forms.Unmarshal(r, &input, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
 	if err != nil {
-		reqErr(w, err, http.StatusBadRequest)
+		writeResponse(w, r, nil, err)
 		return
 	}
-	fmt.Fprintf(w, "%+v\n", input)
+	input.ID = id.ToID().Pack()
+
+	record, err := tgs.UpdateTG(ctx, input)
+	if err != nil {
+		writeResponse(w, r, nil, err)
+		return
+	}
+
+	err = json.NewEncoder(w).Encode(record)
+	if err != nil {
+		writeResponse(w, r, nil, err)
+	}
 }
diff --git a/pkg/database/extend.go b/pkg/database/extend.go
index 3a96cd1..5f165a0 100644
--- a/pkg/database/extend.go
+++ b/pkg/database/extend.go
@@ -9,3 +9,6 @@ func (g GetTalkgroupsWithLearnedRow) GetLearned() bool                   { retur
 func (g GetTalkgroupsWithLearnedBySystemRow) GetTalkgroup() Talkgroup    { return g.Talkgroup }
 func (g GetTalkgroupsWithLearnedBySystemRow) GetSystem() System          { return g.System }
 func (g GetTalkgroupsWithLearnedBySystemRow) GetLearned() bool           { return g.Learned }
+func (g Talkgroup) GetTalkgroup() Talkgroup                              { return g }
+func (g Talkgroup) GetSystem() System                                    { return System{ID: int(g.SystemID)} }
+func (g Talkgroup) GetLearned() bool                                     { return false }
diff --git a/pkg/database/querier.go b/pkg/database/querier.go
index ba1ace1..6a814c3 100644
--- a/pkg/database/querier.go
+++ b/pkg/database/querier.go
@@ -39,6 +39,7 @@ type Querier interface {
 	SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error
 	SetTalkgroupTags(ctx context.Context, sys int, tg int, tags []string) error
 	UpdatePassword(ctx context.Context, username string, password string) error
+	UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error)
 }
 
 var _ Querier = (*Queries)(nil)
diff --git a/pkg/database/talkgroups.sql.go b/pkg/database/talkgroups.sql.go
index b32deb3..81d224f 100644
--- a/pkg/database/talkgroups.sql.go
+++ b/pkg/database/talkgroups.sql.go
@@ -472,3 +472,63 @@ func (q *Queries) SetTalkgroupTags(ctx context.Context, sys int, tg int, tags []
 	_, err := q.db.Exec(ctx, setTalkgroupTags, sys, tg, tags)
 	return err
 }
+
+const updateTalkgroup = `-- name: UpdateTalkgroup :one
+UPDATE talkgroups
+SET
+	name = COALESCE($1, name),
+	alpha_tag = COALESCE($2, alpha_tag),
+	tg_group = COALESCE($3, tg_group),
+	frequency = COALESCE($4, frequency),
+	metadata = COALESCE($5, metadata),
+	tags = COALESCE($6, tags),
+	alert = COALESCE($7, alert),
+	alert_config = COALESCE($8, alert_config),
+	weight = COALESCE($9, weight)
+WHERE id = $10
+RETURNING id, system_id, tgid, name, alpha_tag, tg_group, frequency, metadata, tags, alert, alert_config, weight
+`
+
+type UpdateTalkgroupParams struct {
+	Name        *string  `json:"name"`
+	AlphaTag    *string  `json:"alpha_tag"`
+	TgGroup     *string  `json:"tg_group"`
+	Frequency   *int32   `json:"frequency"`
+	Metadata    []byte   `json:"metadata"`
+	Tags        []string `json:"tags"`
+	Alert       *bool    `json:"alert"`
+	AlertConfig []byte   `json:"alert_config"`
+	Weight      *float32 `json:"weight"`
+	ID          int64    `json:"id"`
+}
+
+func (q *Queries) UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error) {
+	row := q.db.QueryRow(ctx, updateTalkgroup,
+		arg.Name,
+		arg.AlphaTag,
+		arg.TgGroup,
+		arg.Frequency,
+		arg.Metadata,
+		arg.Tags,
+		arg.Alert,
+		arg.AlertConfig,
+		arg.Weight,
+		arg.ID,
+	)
+	var i Talkgroup
+	err := row.Scan(
+		&i.ID,
+		&i.SystemID,
+		&i.Tgid,
+		&i.Name,
+		&i.AlphaTag,
+		&i.TgGroup,
+		&i.Frequency,
+		&i.Metadata,
+		&i.Tags,
+		&i.Alert,
+		&i.AlertConfig,
+		&i.Weight,
+	)
+	return i, err
+}
diff --git a/pkg/talkgroups/cache.go b/pkg/talkgroups/cache.go
index 14c2237..7ada361 100644
--- a/pkg/talkgroups/cache.go
+++ b/pkg/talkgroups/cache.go
@@ -17,7 +17,15 @@ import (
 
 type tgMap map[ID]*Talkgroup
 
+var (
+	ErrNotFound     = errors.New("talkgroup not found")
+	ErrNoSuchSystem = errors.New("no such system")
+)
+
 type Store interface {
+	// UpdateTG updates a talkgroup record.
+	UpdateTG(ctx context.Context, input database.UpdateTalkgroupParams) (*Talkgroup, error)
+
 	// TG retrieves a Talkgroup from the Store.
 	TG(ctx context.Context, tg ID) (*Talkgroup, error)
 
@@ -213,8 +221,6 @@ func (t *cache) Load(ctx context.Context, tgs []int64) error {
 	return nil
 }
 
-var ErrNotFound = errors.New("talkgroup not found")
-
 func (t *cache) Weight(ctx context.Context, id ID, tm time.Time) float64 {
 	tg, err := t.TG(ctx, id)
 	if err != nil {
@@ -290,3 +296,23 @@ func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool)
 
 	return n, has
 }
+
+func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupParams) (*Talkgroup, error) {
+	sysName, has := t.SystemName(ctx, int(Unpack(input.ID).System))
+	if !has {
+		return nil, ErrNoSuchSystem
+	}
+
+	tg, err := database.FromCtx(ctx).UpdateTalkgroup(ctx, input)
+	if err != nil {
+		return nil, err
+	}
+
+	record := &Talkgroup{
+		Talkgroup: tg,
+		System:    database.System{ID: int(tg.SystemID), Name: sysName},
+	}
+	t.add(record)
+
+	return record, nil
+}
diff --git a/pkg/talkgroups/talkgroup.go b/pkg/talkgroups/talkgroup.go
index 288e488..e3cca71 100644
--- a/pkg/talkgroups/talkgroup.go
+++ b/pkg/talkgroups/talkgroup.go
@@ -49,6 +49,13 @@ func (t ID) Pack() int64 {
 	return int64((int64(t.System) << 32) | int64(t.Talkgroup))
 }
 
+func Unpack(id int64) ID {
+	return ID{
+		System:    uint32(id >> 32),
+		Talkgroup: uint32(id & 0xffffffff),
+	}
+}
+
 func (t ID) String() string {
 	return fmt.Sprintf("%d:%d", t.System, t.Talkgroup)
 
diff --git a/sql/postgres/queries/talkgroups.sql b/sql/postgres/queries/talkgroups.sql
index d201d0e..732a848 100644
--- a/sql/postgres/queries/talkgroups.sql
+++ b/sql/postgres/queries/talkgroups.sql
@@ -104,6 +104,20 @@ FROM talkgroups_learned tgl
 JOIN systems sys ON tgl.system_id = sys.id
 WHERE ignored IS NOT TRUE;
 
-
 -- name: GetSystemName :one
 SELECT name FROM systems WHERE id = sqlc.arg(system_id);
+
+-- name: UpdateTalkgroup :one
+UPDATE talkgroups
+SET
+	name = COALESCE(sqlc.narg('name'), name),
+	alpha_tag = COALESCE(sqlc.narg('alpha_tag'), alpha_tag),
+	tg_group = COALESCE(sqlc.narg('tg_group'), tg_group),
+	frequency = COALESCE(sqlc.narg('frequency'), frequency),
+	metadata = COALESCE(sqlc.narg('metadata'), metadata),
+	tags = COALESCE(sqlc.narg('tags'), tags),
+	alert = COALESCE(sqlc.narg('alert'), alert),
+	alert_config = COALESCE(sqlc.narg('alert_config'), alert_config),
+	weight = COALESCE(sqlc.narg('weight'), weight)
+WHERE id = @id
+RETURNING *;