diff --git a/server/errors.go b/server/errors.go
index ddc34629..a00105c3 100644
--- a/server/errors.go
+++ b/server/errors.go
@@ -76,6 +76,7 @@ var (
 	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
 	errHTTPConflictUserExists                        = &errHTTP{40901, http.StatusConflict, "conflict: user already exists", ""}
 	errHTTPConflictTopicReserved                     = &errHTTP{40902, http.StatusConflict, "conflict: access control entry for topic or topic pattern already exists", ""}
+	errHTTPConflictSubscriptionExists                = &errHTTP{40903, http.StatusConflict, "conflict: topic subscription already exists", ""}
 	errHTTPEntityTooLargeAttachment                  = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"}
 	errHTTPEntityTooLargeMatrixRequest               = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", ""}
 	errHTTPEntityTooLargeJSONBody                    = &errHTTP{41303, http.StatusRequestEntityTooLarge, "JSON body too large", ""}
diff --git a/server/server.go b/server/server.go
index 775319e1..288b1388 100644
--- a/server/server.go
+++ b/server/server.go
@@ -39,8 +39,6 @@ import (
   - tiers
   - api
   - tokens
-- LOW: UI: Flickering upgrade banner when logging in
-- LOW: get rid of reservation id, replace with DELETE X-Topic: ...
 
 */
 
@@ -98,7 +96,6 @@ var (
 	apiAccountBillingSubscriptionCheckoutSuccessTemplate = "/v1/account/billing/subscription/success/{CHECKOUT_SESSION_ID}"
 	apiAccountBillingSubscriptionCheckoutSuccessRegex    = regexp.MustCompile(`/v1/account/billing/subscription/success/(.+)$`)
 	apiAccountReservationSingleRegex                     = regexp.MustCompile(`/v1/account/reservation/([-_A-Za-z0-9]{1,64})$`)
-	apiAccountSubscriptionSingleRegex                    = regexp.MustCompile(`^/v1/account/subscription/([-_A-Za-z0-9]{16})$`)
 	staticRegex                                          = regexp.MustCompile(`^/static/.+`)
 	docsRegex                                            = regexp.MustCompile(`^/docs(|/.*)$`)
 	fileRegex                                            = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
@@ -404,9 +401,9 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.ensureUser(s.withAccountSync(s.handleAccountSettingsChange))(w, r, v)
 	} else if r.Method == http.MethodPost && r.URL.Path == apiAccountSubscriptionPath {
 		return s.ensureUser(s.withAccountSync(s.handleAccountSubscriptionAdd))(w, r, v)
-	} else if r.Method == http.MethodPatch && apiAccountSubscriptionSingleRegex.MatchString(r.URL.Path) {
+	} else if r.Method == http.MethodPatch && r.URL.Path == apiAccountSubscriptionPath {
 		return s.ensureUser(s.withAccountSync(s.handleAccountSubscriptionChange))(w, r, v)
-	} else if r.Method == http.MethodDelete && apiAccountSubscriptionSingleRegex.MatchString(r.URL.Path) {
+	} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountSubscriptionPath {
 		return s.ensureUser(s.withAccountSync(s.handleAccountSubscriptionDelete))(w, r, v)
 	} else if r.Method == http.MethodPost && r.URL.Path == apiAccountReservationPath {
 		return s.ensureUser(s.withAccountSync(s.handleAccountReservationAdd))(w, r, v)
diff --git a/server/server_account.go b/server/server_account.go
index e2a9ee80..d7dbd982 100644
--- a/server/server_account.go
+++ b/server/server_account.go
@@ -12,8 +12,6 @@ import (
 )
 
 const (
-	subscriptionIDLength      = 16
-	subscriptionIDPrefix      = "su_"
 	syncTopicAccountSyncEvent = "sync"
 	tokenExpiryDuration       = 72 * time.Hour // Extend tokens by this much
 )
@@ -323,52 +321,36 @@ func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Req
 		return err
 	}
 	u := v.User()
-	if u.Prefs == nil {
-		u.Prefs = &user.Prefs{}
+	prefs := u.Prefs
+	if prefs == nil {
+		prefs = &user.Prefs{}
 	}
-	newSubscription.ID = "" // Client cannot set ID
-	for _, subscription := range u.Prefs.Subscriptions {
+	for _, subscription := range prefs.Subscriptions {
 		if newSubscription.BaseURL == subscription.BaseURL && newSubscription.Topic == subscription.Topic {
-			newSubscription = subscription
-			break
+			return errHTTPConflictSubscriptionExists
 		}
 	}
-	if newSubscription.ID == "" {
-		newSubscription.ID = util.RandomStringPrefix(subscriptionIDPrefix, subscriptionIDLength)
-		prefs := u.Prefs
-		prefs.Subscriptions = append(prefs.Subscriptions, newSubscription)
-		logvr(v, r).
-			Tag(tagAccount).
-			Fields(log.Context{
-				"base_url": newSubscription.BaseURL,
-				"topic":    newSubscription.Topic,
-			}).
-			Debug("Adding subscription for user %s", u.Name)
-		if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
-			return err
-		}
+	prefs.Subscriptions = append(prefs.Subscriptions, newSubscription)
+	logvr(v, r).Tag(tagAccount).With(newSubscription).Debug("Adding subscription for user %s", u.Name)
+	if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
+		return err
 	}
 	return s.writeJSON(w, newSubscription)
 }
 
 func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.Request, v *visitor) error {
-	matches := apiAccountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path)
-	if len(matches) != 2 {
-		return errHTTPInternalErrorInvalidPath
-	}
-	subscriptionID := matches[1]
 	updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit, false)
 	if err != nil {
 		return err
 	}
 	u := v.User()
-	if u.Prefs == nil || u.Prefs.Subscriptions == nil {
+	prefs := u.Prefs
+	if prefs == nil || prefs.Subscriptions == nil {
 		return errHTTPNotFound
 	}
-	prefs := u.Prefs
 	var subscription *user.Subscription
 	for _, sub := range prefs.Subscriptions {
-		if sub.ID == subscriptionID {
+		if sub.BaseURL == updatedSubscription.BaseURL && sub.Topic == updatedSubscription.Topic {
 			sub.DisplayName = updatedSubscription.DisplayName
 			subscription = sub
 			break
@@ -377,14 +359,7 @@ func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.
 	if subscription == nil {
 		return errHTTPNotFound
 	}
-	logvr(v, r).
-		Tag(tagAccount).
-		Fields(log.Context{
-			"base_url":     subscription.BaseURL,
-			"topic":        subscription.Topic,
-			"display_name": subscription.DisplayName,
-		}).
-		Debug("Changing subscription for user %s", u.Name)
+	logvr(v, r).Tag(tagAccount).With(subscription).Debug("Changing subscription for user %s", u.Name)
 	if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
 		return err
 	}
@@ -392,31 +367,23 @@ func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.
 }
 
 func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
-	matches := apiAccountSubscriptionSingleRegex.FindStringSubmatch(r.URL.Path)
-	if len(matches) != 2 {
-		return errHTTPInternalErrorInvalidPath
-	}
-	subscriptionID := matches[1]
+	// DELETEs cannot have a body, and we don't want it in the path
+	deleteBaseURL := readParam(r, "X-BaseURL", "BaseURL")
+	deleteTopic := readParam(r, "X-Topic", "Topic")
 	u := v.User()
-	if u.Prefs == nil || u.Prefs.Subscriptions == nil {
+	prefs := u.Prefs
+	if prefs == nil || prefs.Subscriptions == nil {
 		return nil
 	}
 	newSubscriptions := make([]*user.Subscription, 0)
-	for _, subscription := range u.Prefs.Subscriptions {
-		if subscription.ID == subscriptionID {
-			logvr(v, r).
-				Tag(tagAccount).
-				Fields(log.Context{
-					"base_url": subscription.BaseURL,
-					"topic":    subscription.Topic,
-				}).
-				Debug("Removing subscription for user %s", u.Name)
+	for _, sub := range u.Prefs.Subscriptions {
+		if sub.BaseURL == deleteBaseURL && sub.Topic == deleteTopic {
+			logvr(v, r).Tag(tagAccount).With(sub).Debug("Removing subscription for user %s", u.Name)
 		} else {
-			newSubscriptions = append(newSubscriptions, subscription)
+			newSubscriptions = append(newSubscriptions, sub)
 		}
 	}
-	if len(newSubscriptions) < len(u.Prefs.Subscriptions) {
-		prefs := u.Prefs
+	if len(newSubscriptions) < len(prefs.Subscriptions) {
 		prefs.Subscriptions = newSubscriptions
 		if err := s.userManager.ChangeSettings(u.ID, prefs); err != nil {
 			return err
diff --git a/server/server_account_test.go b/server/server_account_test.go
index c68dd8e7..cc4f4cd5 100644
--- a/server/server_account_test.go
+++ b/server/server_account_test.go
@@ -214,13 +214,11 @@ func TestAccount_Subscription_AddUpdateDelete(t *testing.T) {
 	require.Equal(t, 200, rr.Code)
 	account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
 	require.Equal(t, 1, len(account.Subscriptions))
-	require.NotEmpty(t, account.Subscriptions[0].ID)
 	require.Equal(t, "http://abc.com", account.Subscriptions[0].BaseURL)
 	require.Equal(t, "def", account.Subscriptions[0].Topic)
 	require.Nil(t, account.Subscriptions[0].DisplayName)
 
-	subscriptionID := account.Subscriptions[0].ID
-	rr = request(t, s, "PATCH", "/v1/account/subscription/"+subscriptionID, `{"display_name": "ding dong"}`, map[string]string{
+	rr = request(t, s, "PATCH", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def", "display_name": "ding dong"}`, map[string]string{
 		"Authorization": util.BasicAuth("phil", "phil"),
 	})
 	require.Equal(t, 200, rr.Code)
@@ -231,13 +229,14 @@ func TestAccount_Subscription_AddUpdateDelete(t *testing.T) {
 	require.Equal(t, 200, rr.Code)
 	account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
 	require.Equal(t, 1, len(account.Subscriptions))
-	require.Equal(t, subscriptionID, account.Subscriptions[0].ID)
 	require.Equal(t, "http://abc.com", account.Subscriptions[0].BaseURL)
 	require.Equal(t, "def", account.Subscriptions[0].Topic)
 	require.Equal(t, util.String("ding dong"), account.Subscriptions[0].DisplayName)
 
-	rr = request(t, s, "DELETE", "/v1/account/subscription/"+subscriptionID, "", map[string]string{
+	rr = request(t, s, "DELETE", "/v1/account/subscription", "", map[string]string{
 		"Authorization": util.BasicAuth("phil", "phil"),
+		"X-BaseURL":     "http://abc.com",
+		"X-Topic":       "def",
 	})
 	require.Equal(t, 200, rr.Code)
 
diff --git a/user/manager_test.go b/user/manager_test.go
index 18144aa9..f809b5a9 100644
--- a/user/manager_test.go
+++ b/user/manager_test.go
@@ -726,7 +726,6 @@ func TestManager_ChangeSettings(t *testing.T) {
 		},
 		Subscriptions: []*Subscription{
 			{
-				ID:          "someID",
 				BaseURL:     "https://ntfy.sh",
 				Topic:       "mytopic",
 				DisplayName: util.String("My Topic"),
@@ -742,7 +741,6 @@ func TestManager_ChangeSettings(t *testing.T) {
 	require.Equal(t, util.String("ding"), u.Prefs.Notification.Sound)
 	require.Equal(t, util.Int(2), u.Prefs.Notification.MinPriority)
 	require.Nil(t, u.Prefs.Notification.DeleteAfter)
-	require.Equal(t, "someID", u.Prefs.Subscriptions[0].ID)
 	require.Equal(t, "https://ntfy.sh", u.Prefs.Subscriptions[0].BaseURL)
 	require.Equal(t, "mytopic", u.Prefs.Subscriptions[0].Topic)
 	require.Equal(t, util.String("My Topic"), u.Prefs.Subscriptions[0].DisplayName)
diff --git a/user/types.go b/user/types.go
index 6df1c260..0363c97f 100644
--- a/user/types.go
+++ b/user/types.go
@@ -105,12 +105,19 @@ func (t *Tier) Context() log.Context {
 
 // Subscription represents a user's topic subscription
 type Subscription struct {
-	ID          string  `json:"id"`
 	BaseURL     string  `json:"base_url"`
 	Topic       string  `json:"topic"`
 	DisplayName *string `json:"display_name"`
 }
 
+// Context returns fields for the log
+func (s *Subscription) Context() log.Context {
+	return log.Context{
+		"base_url": s.BaseURL,
+		"topic":    s.Topic,
+	}
+}
+
 // NotificationPrefs represents the user's notification settings
 type NotificationPrefs struct {
 	Sound       *string `json:"sound,omitempty"`
diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js
index 8a78e272..6382d1fa 100644
--- a/web/src/app/AccountApi.js
+++ b/web/src/app/AccountApi.js
@@ -173,9 +173,12 @@ class AccountApi {
         });
     }
 
-    async addSubscription(payload) {
+    async addSubscription(baseUrl, topic) {
         const url = accountSubscriptionUrl(config.base_url);
-        const body = JSON.stringify(payload);
+        const body = JSON.stringify({
+            base_url: baseUrl,
+            topic: topic
+        });
         console.log(`[AccountApi] Adding user subscription ${url}: ${body}`);
         const response = await fetchOrThrow(url, {
             method: "POST",
@@ -187,9 +190,13 @@ class AccountApi {
         return subscription;
     }
 
-    async updateSubscription(remoteId, payload) {
-        const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
-        const body = JSON.stringify(payload);
+    async updateSubscription(baseUrl, topic, payload) {
+        const url = accountSubscriptionUrl(config.base_url);
+        const body = JSON.stringify({
+            base_url: baseUrl,
+            topic: topic,
+            ...payload
+        });
         console.log(`[AccountApi] Updating user subscription ${url}: ${body}`);
         const response = await fetchOrThrow(url, {
             method: "PATCH",
@@ -201,12 +208,16 @@ class AccountApi {
         return subscription;
     }
 
-    async deleteSubscription(remoteId) {
-        const url = accountSubscriptionSingleUrl(config.base_url, remoteId);
+    async deleteSubscription(baseUrl, topic) {
+        const url = accountSubscriptionUrl(config.base_url);
         console.log(`[AccountApi] Removing user subscription ${url}`);
+        const headers = {
+            "X-BaseURL": baseUrl,
+            "X-Topic":  topic,
+        }
         await fetchOrThrow(url, {
             method: "DELETE",
-            headers: withBearerAuth({}, session.token())
+            headers: withBearerAuth(headers, session.token()),
         });
     }
 
diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js
index ef9ff42c..cdfe50e2 100644
--- a/web/src/app/SubscriptionManager.js
+++ b/web/src/app/SubscriptionManager.js
@@ -29,7 +29,6 @@ class SubscriptionManager {
             topic: topic,
             mutedUntil: 0,
             last: null,
-            remoteId: null,
             internal: internal || false
         };
         await db.subscriptions.put(subscription);
@@ -40,24 +39,23 @@ class SubscriptionManager {
         console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions);
 
         // Add remote subscriptions
-        let remoteIds = [];
+        let remoteIds = []; // = topicUrl(baseUrl, topic)
         for (let i = 0; i < remoteSubscriptions.length; i++) {
             const remote = remoteSubscriptions[i];
-            const local = await this.add(remote.base_url, remote.topic);
+            const local = await this.add(remote.base_url, remote.topic, false);
             const reservation = remoteReservations?.find(r => remote.base_url === config.base_url && remote.topic === r.topic) || null;
             await this.update(local.id, {
-                remoteId: remote.id,
                 displayName: remote.display_name, // May be undefined
                 reservation: reservation // May be null!
             });
-            remoteIds.push(remote.id);
+            remoteIds.push(local.id);
         }
 
         // Remove local subscriptions that do not exist remotely
         const localSubscriptions = await db.subscriptions.toArray();
         for (let i = 0; i < localSubscriptions.length; i++) {
             const local = localSubscriptions[i];
-            const remoteExists = local.remoteId && remoteIds.includes(local.remoteId);
+            const remoteExists = remoteIds.includes(local.id);
             if (!local.internal && !remoteExists) {
                 await this.remove(local.id);
             }
@@ -174,12 +172,6 @@ class SubscriptionManager {
         });
     }
 
-    async setRemoteId(subscriptionId, remoteId) {
-        await db.subscriptions.update(subscriptionId, {
-            remoteId: remoteId
-        });
-    }
-
     async setReservation(subscriptionId, reservation) {
         await db.subscriptions.update(subscriptionId, {
             reservation: reservation
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index c53a0f39..88f67ce4 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -23,7 +23,6 @@ export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`;
 export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`;
 export const accountSettingsUrl = (baseUrl) => `${baseUrl}/v1/account/settings`;
 export const accountSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/subscription`;
-export const accountSubscriptionSingleUrl = (baseUrl, id) => `${baseUrl}/v1/account/subscription/${id}`;
 export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reservation`;
 export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`;
 export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js
index b24501bc..7ea00520 100644
--- a/web/src/components/SubscribeDialog.js
+++ b/web/src/components/SubscribeDialog.js
@@ -299,11 +299,7 @@ export const subscribeTopic = async (baseUrl, topic) => {
     const subscription = await subscriptionManager.add(baseUrl, topic);
     if (session.exists()) {
         try {
-            const remoteSubscription = await accountApi.addSubscription({
-                base_url: baseUrl,
-                topic: topic
-            });
-            await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
+            await accountApi.addSubscription(baseUrl, topic);
         } catch (e) {
             console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
             if (e instanceof UnauthorizedError) {
diff --git a/web/src/components/SubscriptionPopup.js b/web/src/components/SubscriptionPopup.js
index e0f3cd52..b33313c9 100644
--- a/web/src/components/SubscriptionPopup.js
+++ b/web/src/components/SubscriptionPopup.js
@@ -109,9 +109,9 @@ export const SubscriptionPopup = (props) => {
     const handleUnsubscribe = async () => {
         console.log(`[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`, props.subscription);
         await subscriptionManager.remove(props.subscription.id);
-        if (session.exists() && props.subscription.remoteId) {
+        if (session.exists() && !subscription.internal) {
             try {
-                await accountApi.deleteSubscription(props.subscription.remoteId);
+                await accountApi.deleteSubscription(props.subscription.baseUrl, props.subscription.topic);
             } catch (e) {
                 console.log(`[SubscriptionPopup] Error unsubscribing`, e);
                 if (e instanceof UnauthorizedError) {
@@ -198,10 +198,10 @@ const DisplayNameDialog = (props) => {
 
     const handleSave = async () => {
         await subscriptionManager.setDisplayName(subscription.id, displayName);
-        if (session.exists() && subscription.remoteId) {
+        if (session.exists() && !subscription.internal) {
             try {
                 console.log(`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`);
-                await accountApi.updateSubscription(subscription.remoteId, { display_name: displayName });
+                await accountApi.updateSubscription(subscription.baseUrl, subscription.topic, { display_name: displayName });
             } catch (e) {
                 console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e);
                 if (e instanceof UnauthorizedError) {
diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js
index c6f85df8..b1ce8ffb 100644
--- a/web/src/components/hooks.js
+++ b/web/src/components/hooks.js
@@ -100,11 +100,7 @@ export const useAutoSubscribe = (subscriptions, selected) => {
                 const subscription = await subscriptionManager.add(baseUrl, params.topic);
                 if (session.exists()) {
                     try {
-                        const remoteSubscription = await accountApi.addSubscription({
-                            base_url: baseUrl,
-                            topic: params.topic
-                        });
-                        await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
+                        await accountApi.addSubscription(baseUrl, params.topic);
                     } catch (e) {
                         console.log(`[Hooks] Auto-subscribing failed`, e);
                         if (e instanceof UnauthorizedError) {