diff --git a/packages/api/src/api/rest/titre-demande.ts b/packages/api/src/api/rest/titre-demande.ts
index 5f5102a0998713bbad71141d801fda6b87fadbf4..6cfa6427f7db62dd8db1a142486cc523f494761b 100644
--- a/packages/api/src/api/rest/titre-demande.ts
+++ b/packages/api/src/api/rest/titre-demande.ts
@@ -12,30 +12,27 @@ import { ETAPE_IS_BROUILLON } from 'camino-common/src/etape'
 import { Effect, pipe, Match } from 'effect'
 import { capitalize } from 'effect/String'
 import { titreDemarcheUpdateTask } from '../../business/titre-demarche-update'
-import { ZodUnparseable } from '../../tools/fp-tools'
 import { CaminoApiError, ITitreEtape } from '../../types'
 import { CreateTitreErrors, CreateDemarcheErrors, createTitre, createDemarche } from './titre-demande.queries'
 import { titreEtapeUpsert } from '../../database/queries/titres-etapes'
 import { getCurrent } from 'camino-common/src/date'
 import { titreEtapeUpdateTask } from '../../business/titre-etape-update'
-import { utilisateurTitreCreate } from '../../database/queries/utilisateurs-titres'
 import { RestNewPostCall } from '../../server/rest'
 import { ETAPES_TYPES } from 'camino-common/src/static/etapesTypes'
 import { ETAPES_STATUTS } from 'camino-common/src/static/etapesStatuts'
 import { TITRES_TYPES_IDS } from 'camino-common/src/static/titresTypes'
+import { updateUtilisateurTitre } from './titres.queries'
 
 type TitreDemandeCreerErrors =
   | 'Accès interdit'
   | 'Permissions insuffisantes'
   | 'Problème lors du lien des titres'
-  | ZodUnparseable
   | CreateTitreErrors
   | 'Problème lors de la mise à jour des taches du titre'
   | CreateDemarcheErrors
   | 'Problème lors de la mise à jour des taches de la démarche'
   | "Problème lors de la création de l'étape"
   | "Problème lors de la mise à jour des tâches de l'étape"
-  | "Problème lors de l'abonnement de l'utilisateur au titre"
   | "L'entreprise est obligatoire"
   | "L'entreprise ne doit pas être présente"
 export const titreDemandeCreer: RestNewPostCall<'/rest/titres'> = (rootPipe): Effect.Effect<TitreDemandeOutput, CaminoApiError<TitreDemandeCreerErrors>> => {
@@ -150,14 +147,7 @@ export const titreDemandeCreer: RestNewPostCall<'/rest/titres'> = (rootPipe): Ef
               },
             })
           }),
-          Effect.tap(() => {
-            return Effect.tryPromise({
-              try: () => utilisateurTitreCreate({ utilisateurId: user.id, titreId }),
-              catch: unknown => {
-                return { message: "Problème lors de l'abonnement de l'utilisateur au titre" as const, extra: unknown }
-              },
-            })
-          }),
+          Effect.tap(() => updateUtilisateurTitre(pool, true, titreId, user.id)),
           Effect.map(etapeId => ({ titreId, etapeId }))
         )
       }
@@ -175,7 +165,6 @@ export const titreDemandeCreer: RestNewPostCall<'/rest/titres'> = (rootPipe): Ef
           'Problème lors de la mise à jour des taches de la démarche',
           "Problème lors de la création de l'étape",
           "Problème lors de la mise à jour des tâches de l'étape",
-          "Problème lors de l'abonnement de l'utilisateur au titre",
           'Les données en base ne correspondent pas à ce qui est attendu',
           () => ({
             ...caminoError,
diff --git a/packages/api/src/api/rest/titres.queries.ts b/packages/api/src/api/rest/titres.queries.ts
index 5849ddd8da9644f14dddcb2e18ada6de1eb346cd..2ba314256f6473be76423c66dc6a2d6d146d88ca 100644
--- a/packages/api/src/api/rest/titres.queries.ts
+++ b/packages/api/src/api/rest/titres.queries.ts
@@ -2,6 +2,8 @@ import { sql } from '@pgtyped/runtime'
 import { getMostRecentValuePropFromEtapeFondamentaleValide, TitreGet, TitreGetDemarche } from 'camino-common/src/titres'
 import { EffectDbQueryAndValidateErrors, Redefine, dbQueryAndValidate, effectDbQueryAndValidate } from '../../pg-database'
 import {
+  IAddUtilisateurTitreDbQuery,
+  IDeleteUtilisateurTitreDbQuery,
   IGetAdministrationsLocalesByTitreIdDbQuery,
   IGetDemarchesByTitreIdQueryDbQuery,
   IGetDoublonsByTitreIdDbQuery,
@@ -15,7 +17,7 @@ import { z } from 'zod'
 import { Commune, communeIdValidator } from 'camino-common/src/static/communes'
 import { Pool } from 'pg'
 import { titreTypeIdValidator } from 'camino-common/src/static/titresTypes'
-import { isAdministration, isSuper, User } from 'camino-common/src/roles'
+import { isAdministration, isSuper, User, UtilisateurId } from 'camino-common/src/roles'
 import { titreStatutIdValidator } from 'camino-common/src/static/titresStatuts'
 import { titreReferenceValidator } from 'camino-common/src/titres-references'
 import {
@@ -514,3 +516,17 @@ export const getTitres = (pool: Pool): Effect.Effect<GetTitre[], CaminoError<Eff
 
 const getTitresDb = sql<Redefine<IGetTitresDbQuery, {}, GetTitre>>`
   select t.id from titres t where t.archive is false`
+
+export const updateUtilisateurTitre = (pool: Pool, abonne: boolean, titreId: TitreId, utilisateurId: UtilisateurId): Effect.Effect<void, CaminoError<EffectDbQueryAndValidateErrors>> =>
+  Effect.if(abonne, {
+    onTrue: () => effectDbQueryAndValidate(addUtilisateurTitreDb, { titreId, utilisateurId }, pool, z.void()),
+    onFalse: () => effectDbQueryAndValidate(deleteUtilisateurTitreDb, { titreId, utilisateurId }, pool, z.void()),
+  })
+
+const addUtilisateurTitreDb = sql<
+  Redefine<IAddUtilisateurTitreDbQuery, { utilisateurId: UtilisateurId; titreId: TitreId }, void>
+>`INSERT INTO utilisateurs__titres (utilisateur_id, titre_id) VALUES ($utilisateurId!, $titreId)`
+
+const deleteUtilisateurTitreDb = sql<
+  Redefine<IDeleteUtilisateurTitreDbQuery, { utilisateurId: UtilisateurId; titreId: TitreId }, void>
+>`DELETE FROM utilisateurs__titres WHERE utilisateur_id=$utilisateurId! and titre_id=$titreId`
diff --git a/packages/api/src/api/rest/titres.queries.types.ts b/packages/api/src/api/rest/titres.queries.types.ts
index d19930efc06169cc3e03a390e960f8c588d8cb08..6852ce711a76133cec23c6e870b5b84214112e6b 100644
--- a/packages/api/src/api/rest/titres.queries.types.ts
+++ b/packages/api/src/api/rest/titres.queries.types.ts
@@ -132,3 +132,33 @@ export interface IGetTitresDbQuery {
   result: IGetTitresDbResult;
 }
 
+/** 'AddUtilisateurTitreDb' parameters type */
+export interface IAddUtilisateurTitreDbParams {
+  titreId?: string | null | void;
+  utilisateurId: string;
+}
+
+/** 'AddUtilisateurTitreDb' return type */
+export type IAddUtilisateurTitreDbResult = void;
+
+/** 'AddUtilisateurTitreDb' query type */
+export interface IAddUtilisateurTitreDbQuery {
+  params: IAddUtilisateurTitreDbParams;
+  result: IAddUtilisateurTitreDbResult;
+}
+
+/** 'DeleteUtilisateurTitreDb' parameters type */
+export interface IDeleteUtilisateurTitreDbParams {
+  titreId?: string | null | void;
+  utilisateurId: string;
+}
+
+/** 'DeleteUtilisateurTitreDb' return type */
+export type IDeleteUtilisateurTitreDbResult = void;
+
+/** 'DeleteUtilisateurTitreDb' query type */
+export interface IDeleteUtilisateurTitreDbQuery {
+  params: IDeleteUtilisateurTitreDbParams;
+  result: IDeleteUtilisateurTitreDbResult;
+}
+
diff --git a/packages/api/src/api/rest/titres.test.integration.ts b/packages/api/src/api/rest/titres.test.integration.ts
index 9803250238f0373e55f6a6ef42047714fedf7009..950248e4829cc2955bcda19504c2d62b2b94cd8f 100644
--- a/packages/api/src/api/rest/titres.test.integration.ts
+++ b/packages/api/src/api/rest/titres.test.integration.ts
@@ -3,7 +3,7 @@ import { titreUpdate } from '../../database/queries/titres'
 import { titreDemarcheCreate } from '../../database/queries/titres-demarches'
 import { titreEtapeCreate } from '../../database/queries/titres-etapes'
 import { userSuper } from '../../database/user-super'
-import { restCall, restDeleteCall, restNewCall, restNewPostCall, restPostCall } from '../../../tests/_utils/index'
+import { restCall, restDeleteCall, restNewCall, restNewPostCall } from '../../../tests/_utils/index'
 import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations'
 import { ITitreDemarche, ITitreEtape } from '../../types'
 import { entreprisesUpsert } from '../../database/queries/entreprises'
@@ -276,24 +276,34 @@ describe('titreModifier', () => {
   })
 
   test('ne peut pas modifier un titre (utilisateur anonyme)', async () => {
-    const tested = await restPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, undefined, { id, nom: 'mon titre modifié', references: [] })
+    const tested = await restNewPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, undefined, { id, nom: 'mon titre modifié', references: [] })
 
-    expect(tested.statusCode).toBe(403)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Accès interdit",
+        "status": 403,
+      }
+    `)
   })
 
   test("ne peut pas modifier un titre (un utilisateur 'entreprise')", async () => {
-    const tested = await restPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, { role: 'entreprise', entrepriseIds: [] }, { id, nom: 'mon titre modifié', references: [] })
+    const tested = await restNewPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, { role: 'entreprise', entrepriseIds: [] }, { id, nom: 'mon titre modifié', references: [] })
 
-    expect(tested.statusCode).toBe(404)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Titre non trouvé",
+        "status": 404,
+      }
+    `)
   })
 
   test('modifie un titre (un utilisateur userSuper)', async () => {
-    const tested = await restPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, { role: 'super' }, { id, nom: 'mon titre modifié', references: [] })
-    expect(tested.statusCode).toBe(204)
+    const tested = await restNewPostCall(dbPool, '/rest/titres/:titreId', { titreId: id }, { role: 'super' }, { id, nom: 'mon titre modifié', references: [] })
+    expect(tested.statusCode, JSON.stringify(tested.body)).toBe(HTTP_STATUS.OK)
   })
 
   test("modifie un titre ARM (un utilisateur 'admin' PTMG)", async () => {
-    const tested = await restPostCall(
+    const tested = await restNewPostCall(
       dbPool,
       '/rest/titres/:titreId',
       { titreId: id },
@@ -303,14 +313,19 @@ describe('titreModifier', () => {
       },
       { id, nom: 'mon titre modifié', references: [] }
     )
-    expect(tested.statusCode).toBe(404)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Titre non trouvé",
+        "status": 404,
+      }
+    `)
   })
 
   test("ne peut pas modifier un titre ARM échu (un utilisateur 'admin' PTMG)", async () => {
     const id = newTitreId()
     await insertTitreGraph({ id, nom: 'mon titre échu', typeId: 'arm', titreStatutId: 'ech', propsTitreEtapesIds: {} })
 
-    const tested = await restPostCall(
+    const tested = await restNewPostCall(
       dbPool,
       '/rest/titres/:titreId',
       { titreId: id },
@@ -320,18 +335,28 @@ describe('titreModifier', () => {
       },
       { id, nom: 'mon titre modifié', references: [] }
     )
-    expect(tested.statusCode).toBe(404)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Titre non trouvé",
+        "status": 404,
+      }
+    `)
   })
 
   test("ne peut pas modifier un titre ARM (un utilisateur 'admin' DGCL/SDFLAE/FL1)", async () => {
-    const tested = await restPostCall(
+    const tested = await restNewPostCall(
       dbPool,
       '/rest/titres/:titreId',
       { titreId: id },
       { role: 'admin', administrationId: ADMINISTRATION_IDS['DGCL/SDFLAE/FL1'] },
       { id, nom: 'mon titre modifié', references: [] }
     )
-    expect(tested.statusCode).toBe(403)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Droits insuffisants pour modifier le titre",
+        "status": 400,
+      }
+    `)
   })
 })
 
@@ -549,9 +574,9 @@ test('utilisateurTitreAbonner', async () => {
     propsTitreEtapesIds: {},
   })
 
-  const tested = await restPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper, { abonne: true })
+  const tested = await restNewPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper, { abonne: true })
 
-  expect(tested.statusCode).toBe(HTTP_STATUS.NO_CONTENT)
+  expect(tested.statusCode, JSON.stringify(tested.body)).toBe(HTTP_STATUS.OK)
 })
 
 test('getUtilisateurTitreAbonner', async () => {
@@ -567,9 +592,9 @@ test('getUtilisateurTitreAbonner', async () => {
   let tested = await restNewCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper)
 
   expect(tested.body).toBe(false)
-  const abonnement = await restPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper, { abonne: true })
+  const abonnement = await restNewPostCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper, { abonne: true })
 
-  expect(abonnement.statusCode).toBe(HTTP_STATUS.NO_CONTENT)
+  expect(abonnement.statusCode, JSON.stringify(abonnement.body)).toBe(HTTP_STATUS.OK)
 
   tested = await restNewCall(dbPool, '/rest/titres/:titreId/abonne', { titreId: id }, userSuper)
 
diff --git a/packages/api/src/api/rest/titres.ts b/packages/api/src/api/rest/titres.ts
index 3f31cf708289ffeba701ef26c4ad20d61d3c60be..61a0ded5431cae9b964e9771c129dabbcf587996 100644
--- a/packages/api/src/api/rest/titres.ts
+++ b/packages/api/src/api/rest/titres.ts
@@ -1,9 +1,9 @@
 import { titreArchive, titresGet, titreGet, titreUpdate } from '../../database/queries/titres'
 import { HTTP_STATUS } from 'camino-common/src/http'
-import { CommonTitreAdministration, editableTitreValidator, TitreLink, TitreLinks, TitreGet, utilisateurTitreAbonneValidator, titreGetValidator, SuperTitre } from 'camino-common/src/titres'
+import { CommonTitreAdministration, TitreLink, TitreLinks, TitreGet, titreGetValidator, SuperTitre } from 'camino-common/src/titres'
 import { CaminoRequest, CustomResponse } from './express-type'
 import { userSuper } from '../../database/user-super'
-import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined, onlyUnique } from 'camino-common/src/typescript-tools'
+import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, onlyUnique } from 'camino-common/src/typescript-tools'
 import { CaminoApiError } from '../../types'
 import { NotNullableKeys, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
 import TitresTitres from '../../database/models/titres--titres'
@@ -17,9 +17,8 @@ import { ETAPES_TYPES, EtapeTypeId } from 'camino-common/src/static/etapesTypes'
 import { CaminoDate, getCurrent } from 'camino-common/src/date'
 import { isAdministration, isSuper, User, UserNotNull } from 'camino-common/src/roles'
 import { canEditDemarche, canCreateTravaux } from 'camino-common/src/permissions/titres-demarches'
-import { utilisateurTitreCreate, utilisateurTitreDelete } from '../../database/queries/utilisateurs-titres'
 import { titreUpdateTask } from '../../business/titre-update'
-import { getDoublonsByTitreId, getTitre as getTitreDb } from './titres.queries'
+import { getDoublonsByTitreId, getTitre as getTitreDb, updateUtilisateurTitre } from './titres.queries'
 import type { Pool } from 'pg'
 import { TitresStatutIds } from 'camino-common/src/static/titresStatuts'
 import { accesSuperSeulementError, getTitresWithBrouillons, GetTitresWithBrouillonsErrors, getTitreUtilisateur } from '../../database/queries/titres-utilisateurs.queries'
@@ -30,6 +29,8 @@ import { CaminoError } from 'camino-common/src/zod-tools'
 import { EffectDbQueryAndValidateErrors } from '../../pg-database'
 import { CaminoMachines, machineFind } from '../../business/rules-demarches/machines'
 import { demarcheEnregistrementDemandeDateFind } from 'camino-common/src/demarche'
+import { DBTitre } from '../../database/models/titres'
+import { AdministrationId } from 'camino-common/src/static/administrations'
 
 const etapesAMasquer = [ETAPES_TYPES.classementSansSuite, ETAPES_TYPES.desistementDuDemandeur, ETAPES_TYPES.demandeDeComplements_RecevabiliteDeLaDemande_]
 
@@ -395,41 +396,28 @@ export const removeTitre =
     }
   }
 
-export const utilisateurTitreAbonner =
-  (_pool: Pool) =>
-  async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => {
-    const user = req.auth
-    const parsedBody = utilisateurTitreAbonneValidator.safeParse(req.body)
-    const titreId: TitreId | undefined | null = titreIdValidator.nullable().optional().parse(req.params.titreId)
-    if (isNullOrUndefined(titreId)) {
-      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-    } else if (!parsedBody.success) {
-      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-    } else {
-      try {
-        if (!user) {
-          res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-        } else {
-          const titre = await titreGet(titreId, { fields: { id: {} } }, user)
-
-          if (!titre) {
-            res.sendStatus(HTTP_STATUS.FORBIDDEN)
-          } else {
-            if (parsedBody.data.abonne) {
-              await utilisateurTitreCreate({ utilisateurId: user.id, titreId })
-            } else {
-              await utilisateurTitreDelete(user.id, titreId)
-            }
-          }
-          res.sendStatus(HTTP_STATUS.NO_CONTENT)
-        }
-      } catch (e) {
-        console.error(e)
-
-        res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
-      }
-    }
-  }
+const erreurLorsDeLaRecuperationDuTitre = 'Erreur lors de la récupération du titre' as const
+type UtilisateurTitreAbonnerErrors = typeof erreurLorsDeLaRecuperationDuTitre | EffectDbQueryAndValidateErrors
+export const utilisateurTitreAbonner: RestNewPostCall<'/rest/titres/:titreId/abonne'> = (rootPipe): Effect.Effect<{ id: TitreId; abonne: boolean }, CaminoApiError<UtilisateurTitreAbonnerErrors>> =>
+  rootPipe.pipe(
+    Effect.bind('titre', ({ user, params }) =>
+      Effect.tryPromise({
+        try: () => titreGet(params.titreId, { fields: { id: {} } }, user),
+        catch: e => ({ message: erreurLorsDeLaRecuperationDuTitre, extra: e }),
+      })
+    ),
+    Effect.tap(({ pool, params, body, user }) => updateUtilisateurTitre(pool, body.abonne, params.titreId, user.id)),
+    Effect.map(({ body, params }) => ({ id: params.titreId, abonne: body.abonne })),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        Match.whenOr('Erreur lors de la récupération du titre', "Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
+          ...caminoError,
+          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
+        })),
+        Match.exhaustive
+      )
+    )
+  )
 
 type GetUtilisateurTitreAbonnerError = EffectDbQueryAndValidateErrors | typeof droitInsuffisant
 export const getUtilisateurTitreAbonner: RestNewGetCall<'/rest/titres/:titreId/abonne'> = (rootPipe): Effect.Effect<boolean, CaminoApiError<GetUtilisateurTitreAbonnerError>> =>
@@ -451,45 +439,70 @@ export const getUtilisateurTitreAbonner: RestNewGetCall<'/rest/titres/:titreId/a
     )
   )
 
-export const updateTitre =
-  (pool: Pool) =>
-  async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => {
-    const titreId: TitreId | undefined | null = titreIdValidator.optional().nullable().parse(req.params.titreId)
-    const user = req.auth
-    const parsedBody = editableTitreValidator.safeParse(req.body)
-    if (!titreId) {
-      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-    } else if (!parsedBody.success) {
-      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-    } else if (titreId !== parsedBody.data.id) {
-      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-    } else {
-      try {
-        const titreOld = await titreGet(titreId, { fields: { pointsEtape: { id: {} } } }, user)
-
-        if (isNullOrUndefined(titreOld)) {
-          res.sendStatus(HTTP_STATUS.NOT_FOUND)
-        } else {
-          if (isNullOrUndefined(titreOld.administrationsLocales)) {
-            throw new Error("pas d'administrations locales chargées")
-          }
-
-          if (!canEditTitre(user, titreOld.typeId, titreOld.titreStatutId, titreOld.administrationsLocales ?? [])) {
-            res.sendStatus(HTTP_STATUS.FORBIDDEN)
-          } else {
-            await titreUpdate(titreId, parsedBody.data)
-
-            await titreUpdateTask(pool, titreId)
-            res.sendStatus(HTTP_STATUS.NO_CONTENT)
-          }
-        }
-      } catch (e) {
-        console.error(e)
+const incoherenceTitreId = "l'id du titre doit être le même que sa route" as const
+const recuperationTitreImpossible = 'Impossible de récupérer le titre' as const
+const titreNonTrouve = 'Titre non trouvé' as const
+const administrationsNonChargees = 'Administrations non chargées' as const
+const droitsInsuffisantsPourModifierLeTitre = 'Droits insuffisants pour modifier le titre' as const
+const erreurPendantLaMiseAJourDuTitre = 'Erreur durant la mise à jour du titre' as const
+type UpdateTitreErrors =
+  | typeof incoherenceTitreId
+  | typeof recuperationTitreImpossible
+  | typeof titreNonTrouve
+  | typeof administrationsNonChargees
+  | typeof droitsInsuffisantsPourModifierLeTitre
+  | typeof erreurPendantLaMiseAJourDuTitre
+export const updateTitre: RestNewPostCall<'/rest/titres/:titreId'> = (rootPipe): Effect.Effect<{ id: TitreId }, CaminoApiError<UpdateTitreErrors>> =>
+  rootPipe.pipe(
+    Effect.filterOrFail(
+      ({ body, params }) => params.titreId === body.id,
+      () => ({ message: incoherenceTitreId })
+    ),
+    Effect.bind('titreOld', ({ user, params }) =>
+      Effect.tryPromise({
+        try: () => titreGet(params.titreId, { fields: { pointsEtape: { id: {} } } }, user),
+        catch: e => ({ message: recuperationTitreImpossible, extra: e }),
+      }).pipe(
+        Effect.filterOrFail(
+          titreOld => isNotNullNorUndefined(titreOld),
+          () => ({ message: titreNonTrouve })
+        ),
+        Effect.filterOrFail(
+          (titreOld): titreOld is DBTitre & { administrationsLocales: AdministrationId[] } => isNotNullNorUndefined(titreOld.administrationsLocales),
+          () => ({ message: administrationsNonChargees })
+        )
+      )
+    ),
+    Effect.filterOrFail(
+      ({ user, titreOld }) => canEditTitre(user, titreOld.typeId, titreOld.titreStatutId, titreOld.administrationsLocales),
+      () => ({ message: droitsInsuffisantsPourModifierLeTitre })
+    ),
+    Effect.tap(({ body, pool }) =>
+      Effect.tryPromise({
+        try: async () => {
+          await titreUpdate(body.id, body)
 
-        res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
-      }
-    }
-  }
+          await titreUpdateTask(pool, body.id)
+        },
+        catch: e => ({ message: erreurPendantLaMiseAJourDuTitre, extra: e }),
+      })
+    ),
+    Effect.map(({ body }) => ({ id: body.id })),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        Match.when('Titre non trouvé', () => ({ ...caminoError, status: HTTP_STATUS.NOT_FOUND })),
+        Match.whenOr('Impossible de récupérer le titre', 'Erreur durant la mise à jour du titre', () => ({
+          ...caminoError,
+          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
+        })),
+        Match.whenOr("l'id du titre doit être le même que sa route", 'Administrations non chargées', 'Droits insuffisants pour modifier le titre', () => ({
+          ...caminoError,
+          status: HTTP_STATUS.BAD_REQUEST,
+        })),
+        Match.exhaustive
+      )
+    )
+  )
 
 export const getTitre =
   (pool: Pool) =>
diff --git a/packages/api/src/database/models/utilisateurs--titres.ts b/packages/api/src/database/models/utilisateurs--titres.ts
deleted file mode 100644
index 08df0387cf7ed7c480bfa12f9d0a5a3a6052fc5e..0000000000000000000000000000000000000000
--- a/packages/api/src/database/models/utilisateurs--titres.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
-import { Model } from 'objection'
-import { IUtilisateurTitre } from '../../types'
-import Utilisateurs from './utilisateurs'
-
-interface UtilisateursTitres extends IUtilisateurTitre {}
-
-class UtilisateursTitres extends Model {
-  public static override tableName = 'utilisateurs__titres'
-
-  public static override jsonSchema = {
-    type: 'object',
-    required: ['utilisateurId', 'titreId'],
-
-    properties: {
-      utilisateurId: { type: 'string' },
-      titreId: { type: 'string' },
-    },
-  }
-
-  public static override idColumn = ['utilisateurId', 'titreId']
-
-  static override relationMappings = () => ({
-    utilisateur: {
-      relation: Model.BelongsToOneRelation,
-      modelClass: Utilisateurs,
-      join: {
-        from: 'utilisateurs__titres.utilisateurId',
-        to: 'utilisateurs.id',
-      },
-    },
-  })
-}
-
-export default UtilisateursTitres
diff --git a/packages/api/src/database/queries/utilisateurs-titres.ts b/packages/api/src/database/queries/utilisateurs-titres.ts
deleted file mode 100644
index eb749b763cef75c0c83f5247a8fc4682a142a254..0000000000000000000000000000000000000000
--- a/packages/api/src/database/queries/utilisateurs-titres.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { UtilisateurId } from 'camino-common/src/roles'
-import { IUtilisateurTitre } from '../../types'
-
-import UtilisateursTitres from '../models/utilisateurs--titres'
-import { TitreId } from 'camino-common/src/validators/titres'
-
-export const utilisateurTitreCreate = async (utilisateurTitre: IUtilisateurTitre): Promise<UtilisateursTitres> => UtilisateursTitres.query().insert(utilisateurTitre)
-
-export const utilisateurTitreDelete = async (utilisateurId: UtilisateurId, titreId: TitreId): Promise<number> => UtilisateursTitres.query().deleteById([utilisateurId, titreId])
diff --git a/packages/api/src/database/queries/utilisateurs.test.integration.ts b/packages/api/src/database/queries/utilisateurs.test.integration.ts
index a31c24625feb2ac7f64133b31cb43b45902ed201..b3f80fe99e6350ef158da007b90ba27846bf8ce3 100644
--- a/packages/api/src/database/queries/utilisateurs.test.integration.ts
+++ b/packages/api/src/database/queries/utilisateurs.test.integration.ts
@@ -9,7 +9,7 @@ import { getCurrent } from 'camino-common/src/date'
 import { callAndExit } from '../../tools/fp-tools'
 import { userSuper } from '../user-super'
 import { createTitre } from '../../api/rest/titre-demande.queries'
-import { utilisateurTitreCreate } from './utilisateurs-titres'
+import { updateUtilisateurTitre } from '../../api/rest/titres.queries'
 console.info = vi.fn()
 console.error = vi.fn()
 
@@ -191,7 +191,7 @@ describe('getUtilisateursByTitreId', () => {
     let result = await getUtilisateursByTitreId(dbPool, titreId)
     expect(result).toHaveLength(0)
 
-    await utilisateurTitreCreate({ titreId, utilisateurId })
+    await callAndExit(updateUtilisateurTitre(dbPool, true, titreId, utilisateurId))
 
     result = await getUtilisateursByTitreId(dbPool, titreId)
     expect(result[0]).toMatchObject({
diff --git a/packages/api/src/server/rest.ts b/packages/api/src/server/rest.ts
index 9d82737a4d4b19c5e9d47ce933942fa76a02c510..353fd1af841d3d879059818e4aad36b49b63643b 100644
--- a/packages/api/src/server/rest.ts
+++ b/packages/api/src/server/rest.ts
@@ -27,7 +27,6 @@ import {
   DownloadFormat,
   contentTypes,
   GetRestRoutes,
-  PostRestRoutes,
   DeleteRestRoutes,
   isCaminoRestRoute,
   DownloadRestRoutes,
@@ -139,13 +138,11 @@ export type RestNewDeleteCall<Route extends NewDeleteRestRoutes> = (
   >
 ) => Effect.Effect<Option.Option<never>, CaminoApiError<string>>
 
-type RestPostCall<Route extends PostRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<CaminoRestRoutesType[Route]['post']['output']>>) => Promise<void>
 type RestDeleteCall = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<void | Error>) => Promise<void>
 type RestDownloadCall = (pool: Pool) => IRestResolver
 
 type Transform<Route> = (Route extends GetRestRoutes ? { getCall: RestGetCall<Route> } : {}) &
   (Route extends NewGetRestRoutes ? { newGetCall: RestNewGetCall<Route> } : {}) &
-  (Route extends PostRestRoutes ? { postCall: RestPostCall<Route> } : {}) &
   (Route extends NewPostRestRoutes ? { newPostCall: RestNewPostCall<Route> } : {}) &
   (Route extends NewPutRestRoutes ? { newPutCall: RestNewPutCall<Route> } : {}) &
   (Route extends DeleteRestRoutes ? { deleteCall: RestDeleteCall } : {}) &
@@ -192,8 +189,8 @@ const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<k
   '/rest/etapesTypes/:demarcheId/:date': { newGetCall: getEtapesTypesEtapesStatusWithMainStep, ...CaminoRestRoutes['/rest/etapesTypes/:demarcheId/:date'] },
   '/rest/quickAccess': { newGetCall: quickAccessSearch, ...CaminoRestRoutes['/rest/quickAccess'] },
   '/rest/titres': { newPostCall: titreDemandeCreer, ...CaminoRestRoutes['/rest/titres'] },
-  '/rest/titres/:titreId': { deleteCall: removeTitre, postCall: updateTitre, getCall: getTitre, ...CaminoRestRoutes['/rest/titres/:titreId'] },
-  '/rest/titres/:titreId/abonne': { postCall: utilisateurTitreAbonner, newGetCall: getUtilisateurTitreAbonner, ...CaminoRestRoutes['/rest/titres/:titreId/abonne'] },
+  '/rest/titres/:titreId': { deleteCall: removeTitre, newPostCall: updateTitre, getCall: getTitre, ...CaminoRestRoutes['/rest/titres/:titreId'] },
+  '/rest/titres/:titreId/abonne': { newPostCall: utilisateurTitreAbonner, newGetCall: getUtilisateurTitreAbonner, ...CaminoRestRoutes['/rest/titres/:titreId/abonne'] },
   '/rest/titresAdministrations': { getCall: titresAdministrations, ...CaminoRestRoutes['/rest/titresAdministrations'] },
   '/rest/titresSuper': { newGetCall: titresSuper, ...CaminoRestRoutes['/rest/titresSuper'] },
   '/rest/statistiques/minerauxMetauxMetropole': { getCall: getMinerauxMetauxMetropolesStats, ...CaminoRestRoutes['/rest/statistiques/minerauxMetauxMetropole'] }, // UNTESTED YET
@@ -328,11 +325,6 @@ export const restWithPool = (dbPool: Pool): Router => {
           }
         })
       }
-      if ('postCall' in maRoute) {
-        console.info(`POST ${route}`)
-        rest.post(route, restCatcherWithMutation('post', maRoute.postCall(dbPool), dbPool)) // eslint-disable-line @typescript-eslint/no-misused-promises
-      }
-
       if ('newPostCall' in maRoute) {
         console.info(`POST ${route}`)
         // eslint-disable-next-line @typescript-eslint/no-misused-promises
diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts
index 0381df06dea89e69f7e0e68683879990cbf5698b..daa20429336353f9121301bc8c2a2b5103dc74f8 100644
--- a/packages/api/src/types.ts
+++ b/packages/api/src/types.ts
@@ -271,11 +271,6 @@ export interface ITitreEtapeFiltre {
   dateFin?: string
 }
 
-export interface IUtilisateurTitre {
-  utilisateurId: string
-  titreId: string
-}
-
 export type Context = { user: User; pool: Pool }
 
 export interface IJournaux {
diff --git a/packages/api/tests/_utils/index.ts b/packages/api/tests/_utils/index.ts
index 93eba7dd11e9f1f9213e4a785d7d15b6578f314e..702a8a7f4a4ab8a00f8b550e7c889c0db3179d56 100644
--- a/packages/api/tests/_utils/index.ts
+++ b/packages/api/tests/_utils/index.ts
@@ -15,7 +15,6 @@ import {
   DeleteRestRoutes,
   getRestRoute,
   GetRestRoutes,
-  PostRestRoutes,
   CaminoRestParams,
   DownloadRestRoutes,
   NewPostRestRoutes,
@@ -83,17 +82,6 @@ export const restNewCall = async <Route extends NewGetRestRoutes>(
   return jwtSet(pool, req, user)
 }
 
-export const restPostCall = async <Route extends PostRestRoutes>(
-  pool: Pool,
-  caminoRestRoute: Route,
-  params: CaminoRestParams<Route>,
-  user: TestUser | undefined,
-  body: z.infer<(typeof CaminoRestRoutes)[Route]['post']['input']>
-): Promise<request.Test> => {
-  const req = request(app(pool)).post(getRestRoute(caminoRestRoute, params)).send(body)
-
-  return jwtSet(pool, req, user)
-}
 export const restNewPostCall = async <Route extends NewPostRestRoutes>(
   pool: Pool,
   caminoRestRoute: Route,
diff --git a/packages/common/src/rest.ts b/packages/common/src/rest.ts
index 78f248921e440a7869745da1ce84bc58b8dd4adf..b2117f08ea3113e7fdc41df42a2371a110941686 100644
--- a/packages/common/src/rest.ts
+++ b/packages/common/src/rest.ts
@@ -72,7 +72,6 @@ import { flattenEtapeValidator, restEtapeCreationValidator, restEtapeModificatio
 type CaminoRoute<T extends string> = { params: ZodObjectParsUrlParams<T> } & {
   get?: { output: ZodType; searchParams?: ZodType }
   newGet?: { output: ZodType; searchParams?: ZodType }
-  post?: { input: ZodType; output: ZodType }
   newPost?: { input: ZodType; output: ZodType }
   newPut?: { input: ZodType; output: ZodType }
   newDelete?: true
@@ -176,8 +175,17 @@ export const CaminoRestRoutes = {
   '/rest/statistiques/datagouv': { params: noParamsValidator, get: { output: z.array(statistiquesDataGouvValidator) } },
   '/rest/quickAccess': { params: noParamsValidator, newGet: { searchParams: quickAccessSearchParamsValidator, output: quickAccessArrayResultValidator } },
   '/rest/titres': { params: noParamsValidator, newPost: { input: titreDemandeValidator, output: titreDemandeOutputValidator } },
-  '/rest/titres/:titreId': { params: z.object({ titreId: titreIdOrSlugValidator }), get: { output: titreGetValidator }, delete: true, post: { output: z.void(), input: editableTitreValidator } },
-  '/rest/titres/:titreId/abonne': { params: z.object({ titreId: titreIdValidator }), post: { input: utilisateurTitreAbonneValidator, output: z.void() }, newGet: { output: z.boolean() } },
+  '/rest/titres/:titreId': {
+    params: z.object({ titreId: titreIdOrSlugValidator }),
+    get: { output: titreGetValidator },
+    delete: true,
+    newPost: { output: z.object({ id: titreIdValidator }), input: editableTitreValidator },
+  },
+  '/rest/titres/:titreId/abonne': {
+    params: z.object({ titreId: titreIdValidator }),
+    newPost: { input: utilisateurTitreAbonneValidator, output: z.object({ id: titreIdValidator, abonne: z.boolean() }) },
+    newGet: { output: z.boolean() },
+  },
   '/rest/titresAdministrations': { params: noParamsValidator, get: { output: z.array(titreAdministrationValidator) } },
   '/rest/titresSuper': { params: noParamsValidator, newGet: { output: z.array(superTitreValidator) } },
   '/rest/titres/:id/titreLiaisons': { params: z.object({ id: titreIdValidator }), newGet: { output: titreLinksValidator }, newPost: { input: z.array(titreIdValidator), output: titreLinksValidator } },
@@ -310,7 +318,6 @@ type CaminoRestRouteList<Route, Method extends MethodVerb> = Route extends reado
 
 export type GetRestRoutes = CaminoRestRouteList<typeof IDS, 'get'>[number]
 export type NewGetRestRoutes = CaminoRestRouteList<typeof IDS, 'newGet'>[number]
-export type PostRestRoutes = CaminoRestRouteList<typeof IDS, 'post'>[number]
 export type NewPostRestRoutes = CaminoRestRouteList<typeof IDS, 'newPost'>[number]
 export type NewPutRestRoutes = CaminoRestRouteList<typeof IDS, 'newPut'>[number]
 export type DeleteRestRoutes = CaminoRestRouteList<typeof IDS, 'delete'>[number]
diff --git a/packages/ui/src/api/client-rest.ts b/packages/ui/src/api/client-rest.ts
index 1128d1874bc7deed26eb4d8d6c6dd991e653ce2c..e6f4d1a9b335edce47447abafa6347af9ae2000a 100644
--- a/packages/ui/src/api/client-rest.ts
+++ b/packages/ui/src/api/client-rest.ts
@@ -8,7 +8,6 @@ import {
   NewDownloadRestRoutes,
   getRestRoute,
   GetRestRoutes,
-  PostRestRoutes,
   NewPostRestRoutes,
   NewGetRestRoutes,
   NewPutRestRoutes,
@@ -194,15 +193,6 @@ export const newDeleteWithJson = async <T extends NewDeleteRestRoutes>(path: T,
 export const deleteWithJson = async <T extends DeleteRestRoutes>(path: T, params: CaminoRestParams<T>, searchParams: Record<string, string | string[]> = {}): Promise<void> =>
   callFetch(path, params, 'delete', searchParams)
 
-/**
- * @deprecated use newPostWithJson
- **/
-export const postWithJson = async <T extends PostRestRoutes>(
-  path: T,
-  params: CaminoRestParams<T>,
-  body: z.infer<(typeof CaminoRestRoutes)[T]['post']['input']>
-): Promise<z.infer<(typeof CaminoRestRoutes)[T]['post']['output']>> => callFetch(path, params, 'post', {}, body)
-
 export const newPostWithJson = async <T extends NewPostRestRoutes>(
   path: T,
   params: CaminoRestParams<T>,
diff --git a/packages/ui/src/components/titre.stories.tsx b/packages/ui/src/components/titre.stories.tsx
index d36f31c9a384988cb1f4a7969a7eb832782f500d..4ea689d83953680e8da401a34001c8085b40b35b 100644
--- a/packages/ui/src/components/titre.stories.tsx
+++ b/packages/ui/src/components/titre.stories.tsx
@@ -216,10 +216,10 @@ const titre = {
 } as const satisfies TitreGet
 
 const apiClient: PropsApiClient = {
-  editTitre: (...params) => {
+  editTitre: params => {
     editTitreAction(params)
 
-    return Promise.resolve()
+    return Promise.resolve({ id: params.id })
   },
   deleteDemarche: (...params) => {
     deleteDemarcheAction(params)
@@ -246,10 +246,10 @@ const apiClient: PropsApiClient = {
 
     return Promise.resolve(true)
   },
-  titreUtilisateurAbonne: (...params) => {
-    titreUtilisateurAbonneAction(params)
+  titreUtilisateurAbonne: (params, abonne) => {
+    titreUtilisateurAbonneAction(params, abonne)
 
-    return Promise.resolve()
+    return Promise.resolve({ id: params, abonne })
   },
   loadLinkableTitres:
     (...params) =>
diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html
index dd1f3f99a5a845276d617003d13ebe4c0ca6d50c..8127e6df8671ea7f5c0c5e3b38caeb7f8fda8cc9 100644
--- a/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html
+++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieMutation.html
@@ -7,7 +7,7 @@
           <!---->
         </button><button class="fr-btn fr-btn--secondary fr-btn--md fr-icon-delete-bin-line fr-ml-2w" title="Supprimer le titre" aria-label="Supprimer le titre" type="button">
           <!---->
-        </button><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S’abonner" aria-label="S’abonner" type="button" style="margin-right: 0px;">S’abonner</button></div>
+        </button><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S'abonner" aria-label="S'abonner" type="button" style="margin-right: 0px;">S'abonner</button></div>
     </div>
     <div class="fr-grid-row fr-grid-row--middle fr-mt-1w">
       <p class="fr-h3 fr-m-0">Concession</p>
diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html
index 623c22f7028478f24b0a797cdf4aa02406e1bba6..9cbb892ba320871d4313c040aebc8a4da2b1c703 100644
--- a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html
+++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroi.html
@@ -6,7 +6,7 @@
       <div class="fr-m-0" style="margin-left: auto; display: flex;"><a class="fr-btn fr-btn--secondary fr-pl-2w fr-pr-2w fr-link" title="Signaler une erreur" href="mailto:camino@beta.gouv.fr?subject=Erreur m-cx-chantepie-1988&amp;body=Bonjour, j'ai repéré une erreur sur le titre http://localhost:3000/ :" target="_blank" style="margin-left: auto; display: flex;" rel="noopener external">Signaler une erreur</a><button class="fr-btn fr-btn--secondary fr-btn--md fr-icon-pencil-line fr-ml-2w" title="Éditer le titre" aria-label="Éditer le titre" type="button">
           <!---->
         </button>
-        <!----><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S’abonner" aria-label="S’abonner" type="button" style="margin-right: 0px;">S’abonner</button>
+        <!----><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S'abonner" aria-label="S'abonner" type="button" style="margin-right: 0px;">S'abonner</button>
       </div>
     </div>
     <div class="fr-grid-row fr-grid-row--middle fr-mt-1w">
diff --git a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html
index 3fd0efc376ddf048801234f29ff339d47a07a69b..750b4927d90fcf06b320272f45dd919cb0a8bb53 100644
--- a/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html
+++ b/packages/ui/src/components/titre.stories_snapshots_ChantepieOctroiAsEntreprise.html
@@ -5,7 +5,7 @@
       <p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; align-self: center;" class="fr-badge fr-badge--md fr-ml-2w fr-badge--green-bourgeon fr-ml-2w" title="valide" aria-label="valide">valide</p>
       <div class="fr-m-0" style="margin-left: auto; display: flex;"><a class="fr-btn fr-btn--secondary fr-pl-2w fr-pr-2w fr-link" title="Signaler une erreur" href="mailto:camino@beta.gouv.fr?subject=Erreur m-cx-chantepie-1988&amp;body=Bonjour, j'ai repéré une erreur sur le titre http://localhost:3000/ :" target="_blank" style="margin-left: auto; display: flex;" rel="noopener external">Signaler une erreur</a>
         <!---->
-        <!----><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S’abonner" aria-label="S’abonner" type="button" style="margin-right: 0px;">S’abonner</button>
+        <!----><button class="fr-btn fr-btn--primary fr-btn--md fr-ml-2w" title="S'abonner" aria-label="S'abonner" type="button" style="margin-right: 0px;">S'abonner</button>
       </div>
     </div>
     <div class="fr-grid-row fr-grid-row--middle fr-mt-1w">
diff --git a/packages/ui/src/components/titre/edit-popup.stories.tsx b/packages/ui/src/components/titre/edit-popup.stories.tsx
index 8a7dc5ccbd9a09f545562f9f1230a263bf766649..bc9bd047a4a6b0913c049efdac9f6b9d07573cc0 100644
--- a/packages/ui/src/components/titre/edit-popup.stories.tsx
+++ b/packages/ui/src/components/titre/edit-popup.stories.tsx
@@ -29,10 +29,10 @@ export const DefaultNoReference: StoryFn = () => (
     }}
     close={close}
     apiClient={{
-      editTitre: (...params) => {
+      editTitre: params => {
         editTitreAction(params)
 
-        return Promise.resolve()
+        return Promise.resolve({ id: params.id })
       },
     }}
   />
@@ -48,10 +48,10 @@ export const OneReference: StoryFn = () => (
     }}
     close={close}
     apiClient={{
-      editTitre: (...params) => {
+      editTitre: params => {
         editTitreAction(params)
 
-        return Promise.resolve()
+        return Promise.resolve({ id: params.id })
       },
     }}
   />
diff --git a/packages/ui/src/components/titre/edit-popup.tsx b/packages/ui/src/components/titre/edit-popup.tsx
index 077a716d813210ebe809ca633b9ae8b954b31b5b..5d2823f48eb401020c6d4f39755686117b1c4844 100644
--- a/packages/ui/src/components/titre/edit-popup.tsx
+++ b/packages/ui/src/components/titre/edit-popup.tsx
@@ -38,12 +38,16 @@ export const EditPopup = defineComponent<Props>(props => {
       close={props.close}
       validate={{
         action: async () => {
-          await props.apiClient.editTitre({
+          const value = await props.apiClient.editTitre({
             id: props.titre.id,
             nom: nom.value,
             references: references.value,
           })
+          if ('message' in value) {
+            return value
+          }
           props.reload()
+          return value
         },
         text: 'Enregistrer',
       }}
diff --git a/packages/ui/src/components/titre/titre-abonner-button.stories.tsx b/packages/ui/src/components/titre/titre-abonner-button.stories.tsx
index 421e2ad43b6220ecf14d9024b761179cf7f58d1f..403a416bef650adaed33a762cfca8836808d65d5 100644
--- a/packages/ui/src/components/titre/titre-abonner-button.stories.tsx
+++ b/packages/ui/src/components/titre/titre-abonner-button.stories.tsx
@@ -24,7 +24,7 @@ const apiClient: Pick<TitreApiClient, 'getTitreUtilisateurAbonne' | 'titreUtilis
   titreUtilisateurAbonne: (...params) => {
     titreUtilisateurAbonneAction(params)
 
-    return Promise.resolve()
+    return Promise.resolve({ id: params[0], abonne: params[1] })
   },
 }
 const titreId = titreIdValidator.parse('titreId')
@@ -55,6 +55,6 @@ export const WithError: StoryFn = () => (
   <TitreAbonnerButton
     user={{ ...testBlankUser, role: 'super' }}
     titreId={titreId}
-    apiClient={{ ...apiClient, getTitreUtilisateurAbonne: () => Promise.reject(new Error('Une erreur est survenue')) }}
+    apiClient={{ ...apiClient, getTitreUtilisateurAbonne: () => Promise.resolve({ message: 'Une erreur est survenue' }) }}
   />
 )
diff --git a/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_NotAbonne.html b/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_NotAbonne.html
index 4faa39c54b1687c4f6430aa0e97c8b37f6137c1c..ab1b2c34bae5364d7a4441aa943685a46e281504 100644
--- a/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_NotAbonne.html
+++ b/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_NotAbonne.html
@@ -1 +1 @@
-<button class="fr-btn fr-btn--primary fr-btn--md" title="S’abonner" aria-label="S’abonner" type="button" style="margin-right: 0px;">S’abonner</button>
\ No newline at end of file
+<button class="fr-btn fr-btn--primary fr-btn--md" title="S'abonner" aria-label="S'abonner" type="button" style="margin-right: 0px;">S'abonner</button>
\ No newline at end of file
diff --git a/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_WithError.html b/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_WithError.html
index 5cd6a49518801e7d551131667e88a40de522a282..96ab73995c352d5866bce731b0fffe7f6fcbc61a 100644
--- a/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_WithError.html
+++ b/packages/ui/src/components/titre/titre-abonner-button.stories_snapshots_WithError.html
@@ -1,7 +1,7 @@
 <div class="" style="display: flex; justify-content: center;">
-  <div class="fr-alert fr-alert--error fr-alert--sm">
+  <!---->
+  <div class="fr-alert fr-alert--error fr-alert--sm" role="alert">
     <p>Une erreur est survenue</p>
   </div>
   <!---->
-  <!---->
 </div>
\ No newline at end of file
diff --git a/packages/ui/src/components/titre/titre-abonner-button.tsx b/packages/ui/src/components/titre/titre-abonner-button.tsx
index fea7499039afd80ca0af795d0a37d8f232e0a8ec..7a9f19c4999190919658731c3468b02eea700bc1 100644
--- a/packages/ui/src/components/titre/titre-abonner-button.tsx
+++ b/packages/ui/src/components/titre/titre-abonner-button.tsx
@@ -1,4 +1,4 @@
-import { HTMLAttributes, defineComponent, onMounted, ref } from 'vue'
+import { HTMLAttributes, defineComponent, onMounted } from 'vue'
 import { TitreId } from 'camino-common/src/validators/titres'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 import { User } from 'camino-common/src/roles'
@@ -6,6 +6,7 @@ import { DsfrButton } from '../_ui/dsfr-button'
 import { LoadingElement } from '../_ui/functional-loader'
 import { TitreApiClient } from './titre-api-client'
 import { AsyncData } from '../../api/client-rest'
+import { useState } from '@/utils/vue-tsx-utils'
 
 type Props = {
   apiClient: Pick<TitreApiClient, 'getTitreUtilisateurAbonne' | 'titreUtilisateurAbonne'>
@@ -15,12 +16,16 @@ type Props = {
 }
 
 export const TitreAbonnerButton = defineComponent<Props>(props => {
-  const data = ref<AsyncData<boolean>>({ status: 'LOADING' })
+  const [data, setData] = useState<AsyncData<boolean>>({ status: 'LOADING' })
 
   const toggleAbonner = async () => {
     if (data.value.status === 'LOADED') {
-      await props.apiClient.titreUtilisateurAbonne(props.titreId, !data.value.value)
-      data.value.value = !data.value.value
+      const result = await props.apiClient.titreUtilisateurAbonne(props.titreId, !data.value.value)
+      if ('message' in result) {
+        setData({ status: 'NEW_ERROR', error: result })
+      } else {
+        setData({ status: 'LOADED', value: !data.value.value })
+      }
     }
   }
 
@@ -29,17 +34,17 @@ export const TitreAbonnerButton = defineComponent<Props>(props => {
       if (isNotNullNorUndefined(props.user)) {
         const abonne = await props.apiClient.getTitreUtilisateurAbonne(props.titreId)
         if (typeof abonne !== 'boolean') {
-          data.value = { status: 'NEW_ERROR', error: abonne }
+          setData({ status: 'NEW_ERROR', error: abonne })
         } else {
-          data.value = { status: 'LOADED', value: abonne }
+          setData({ status: 'LOADED', value: abonne })
         }
       }
     } catch (e: any) {
       console.error('error', e)
-      data.value = {
+      setData({
         status: 'ERROR',
         message: e.message ?? 'something wrong happened',
-      }
+      })
     }
   })
 
@@ -49,7 +54,7 @@ export const TitreAbonnerButton = defineComponent<Props>(props => {
         <LoadingElement
           data={data.value}
           renderItem={isAbonne => (
-            <DsfrButton class={props.class} style={{ marginRight: 0 }} buttonType={isAbonne ? 'secondary' : 'primary'} title={isAbonne ? 'Se désabonner' : 'S’abonner'} onClick={toggleAbonner} />
+            <DsfrButton class={props.class} style={{ marginRight: 0 }} buttonType={isAbonne ? 'secondary' : 'primary'} title={isAbonne ? 'Se désabonner' : "S'abonner"} onClick={toggleAbonner} />
           )}
         />
       ) : null}
diff --git a/packages/ui/src/components/titre/titre-api-client.ts b/packages/ui/src/components/titre/titre-api-client.ts
index 1595e19107e99c49fda1b44b4cd4377b66cef8b0..b34c9827bc81d789de659581a865abb014a424f5 100644
--- a/packages/ui/src/components/titre/titre-api-client.ts
+++ b/packages/ui/src/components/titre/titre-api-client.ts
@@ -1,6 +1,6 @@
 import { EditableTitre, QuickAccessResult, TitreDemande, TitreDemandeOutput, TitreGet } from 'camino-common/src/titres'
 import { TitreId, TitreIdOrSlug } from 'camino-common/src/validators/titres'
-import { deleteWithJson, getWithJson, newGetWithJson, newPostWithJson, postWithJson } from '../../api/client-rest'
+import { deleteWithJson, getWithJson, newGetWithJson, newPostWithJson } from '../../api/client-rest'
 import { CaminoDate } from 'camino-common/src/date'
 import { CommuneId } from 'camino-common/src/static/communes'
 import { EntrepriseId } from 'camino-common/src/entreprise'
@@ -42,9 +42,9 @@ type TitreForTitresRerchercherByNom = {
 
 export interface TitreApiClient {
   removeTitre: (titreId: TitreId) => Promise<void>
-  titreUtilisateurAbonne: (titreId: TitreId, abonne: boolean) => Promise<void>
+  titreUtilisateurAbonne: (titreId: TitreId, abonne: boolean) => Promise<{ id: TitreId; abonne: boolean } | CaminoError<string>>
   getTitreUtilisateurAbonne: (titreId: TitreId) => Promise<boolean | CaminoError<string>>
-  editTitre: (titre: EditableTitre) => Promise<void>
+  editTitre: (titre: EditableTitre) => Promise<{ id: TitreId } | CaminoError<string>>
   getTitreById: (titreId: TitreIdOrSlug) => Promise<TitreGet>
   getTitresForTable: (params: {
     page?: number
@@ -111,14 +111,14 @@ export const titreApiClient: TitreApiClient = {
   removeTitre: async (titreId: TitreId): Promise<void> => {
     return deleteWithJson('/rest/titres/:titreId', { titreId })
   },
-  titreUtilisateurAbonne: async (titreId: TitreId, abonne: boolean): Promise<void> => {
-    return postWithJson('/rest/titres/:titreId/abonne', { titreId }, { abonne })
+  titreUtilisateurAbonne: async (titreId: TitreId, abonne: boolean): Promise<{ id: TitreId; abonne: boolean } | CaminoError<string>> => {
+    return newPostWithJson('/rest/titres/:titreId/abonne', { titreId }, { abonne })
   },
   getTitreUtilisateurAbonne: async (titreId: TitreId): Promise<boolean | CaminoError<string>> => {
     return newGetWithJson('/rest/titres/:titreId/abonne', { titreId })
   },
-  editTitre: (titre: EditableTitre): Promise<void> => {
-    return postWithJson('/rest/titres/:titreId', { titreId: titre.id }, titre)
+  editTitre: (titre: EditableTitre): Promise<{ id: TitreId } | CaminoError<string>> => {
+    return newPostWithJson('/rest/titres/:titreId', { titreId: titre.id }, titre)
   },
   getTitreById: (titreId: TitreIdOrSlug): Promise<TitreGet> => {
     return getWithJson('/rest/titres/:titreId', { titreId })