diff --git a/package-lock.json b/package-lock.json
index 022949ae6df00d05a432aae6baeabede35b93daf..937012a894d6849c8999780504b6a1df9a1cefe6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14544,6 +14544,11 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/fp-ts": {
+      "version": "2.16.6",
+      "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.6.tgz",
+      "integrity": "sha512-v7w209VPj4L6pPn/ftFRJu31Oa8QagwcVw7BZmLCUWU4AQoc954rX9ogSIahDf67Pg+GjPbkW/Kn9XWnlWJG0g=="
+    },
     "node_modules/frac": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
@@ -28010,6 +28015,17 @@
         "url": "https://github.com/sponsors/colinhacks"
       }
     },
+    "node_modules/zod-validation-error": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.3.0.tgz",
+      "integrity": "sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw==",
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "peerDependencies": {
+        "zod": "^3.18.0"
+      }
+    },
     "packages/api": {
       "name": "camino-api",
       "version": "1.0.0",
@@ -28043,6 +28059,7 @@
         "express": "^4.19.2",
         "express-jwt": "^8.4.1",
         "express-rate-limit": "^7.2.0",
+        "fp-ts": "^2.16.6",
         "graphql": "^16.8.1",
         "graphql-fields": "^2.0.3",
         "graphql-http": "^1.22.1",
@@ -28065,7 +28082,8 @@
         "ts-node": "^10.9.2",
         "xlsx": "^0.18.5",
         "xstate": "^5.12.0",
-        "zod": "^3.23.4"
+        "zod": "^3.23.4",
+        "zod-validation-error": "^3.3.0"
       },
       "devDependencies": {
         "@types/carbone": "^3.2.5",
diff --git a/packages/api/package.json b/packages/api/package.json
index b6c33d5598c1753c149e72171a36dea92cda9183..90b4b78f38e3b43c0f0cc3933a5dac9229b7fc83 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -67,6 +67,7 @@
     "express": "^4.19.2",
     "express-jwt": "^8.4.1",
     "express-rate-limit": "^7.2.0",
+    "fp-ts": "^2.16.6",
     "graphql": "^16.8.1",
     "graphql-fields": "^2.0.3",
     "graphql-http": "^1.22.1",
@@ -89,7 +90,8 @@
     "ts-node": "^10.9.2",
     "xlsx": "^0.18.5",
     "xstate": "^5.12.0",
-    "zod": "^3.23.4"
+    "zod": "^3.23.4",
+    "zod-validation-error": "^3.3.0"
   },
   "devDependencies": {
     "@types/carbone": "^3.2.5",
@@ -120,6 +122,9 @@
     "printWidth": 200
   },
   "eslintConfig": {
+    "globals": {
+        "GeoJSON": "readonly"
+      },
     "parser": "@typescript-eslint/parser",
     "parserOptions": {
       "project": true
diff --git a/packages/api/src/api/rest/__snapshots__/perimetre.test.integration.ts.snap b/packages/api/src/api/rest/__snapshots__/perimetre.test.integration.ts.snap
index 672c1f0e898a50d256b4186bf900b7f3a8d2bc92..90002c8f02aa82efc10b646661ad665f5c1d8298 100644
--- a/packages/api/src/api/rest/__snapshots__/perimetre.test.integration.ts.snap
+++ b/packages/api/src/api/rest/__snapshots__/perimetre.test.integration.ts.snap
@@ -1,1138 +1,5 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`convertGeojsonPointsToGeoSystemeId > toutes les conversions 1`] = `
-{
-  "2154": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -6508915.664375298,
-            4155227.2486715293,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6509838.379601351,
-            4156321.503214284,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6508315.743755955,
-            4158116.456688329,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "27561": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -6705121.61250076,
-            -2533464.314156466,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6706045.365292369,
-            -2532311.7342340853,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6704428.434049787,
-            -2530499.1943537975,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "27563": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -6419903.998539594,
-            -2132422.3744552927,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6420844.302907188,
-            -2131387.1943672756,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6419422.26215663,
-            -2129579.828581271,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "27571": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -6705121.61250076,
-            -1533464.3141564662,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6706045.365292369,
-            -1532311.7342340853,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6704428.434049787,
-            -1530499.1943537975,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "27572": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -6558012.142644974,
-            -330085.70153978,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6558943.952043006,
-            -328992.23226204794,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6557425.0718446905,
-            -327182.74701307714,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "27573": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -6419903.998539594,
-            867577.6255447073,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6420844.302907188,
-            868612.8056327244,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -6419422.26215663,
-            870420.1714187288,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "2970": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            1667693.002642631,
-            474962.27304501686,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            1666562.013422814,
-            475136.9208461822,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            1666536.3821532296,
-            477018.79235749214,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "2972": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            329078.1462566799,
-            466912.7568635224,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            327968.36777390563,
-            467101.87921943853,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            327972.06552555226,
-            468953.34014603146,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "2975": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -10635907.529523982,
-            28583935.690351125,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -10632537.837798638,
-            28584100.0361242,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -10631349.492396124,
-            28578682.38840603,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "32620": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            1667272.1766110498,
-            474656.11260267516,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            1666141.1911915715,
-            474830.76448165777,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            1666115.5675545805,
-            476712.63103764126,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "32621": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            995446.594222497,
-            468165.6553138382,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            994332.421942425,
-            468346.6813927581,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            994321.7585003038,
-            470203.04016723786,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "32622": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            329078.14625669573,
-            466912.7568788742,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            327968.3677739216,
-            467101.8792347965,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            327972.06552556844,
-            468953.3401614499,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "32630": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -5841798.951395092,
-            720788.3745672021,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -5843479.1534257,
-            721224.1298748096,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -5843230.650385964,
-            724068.2261216913,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "3313": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            995446.5942225576,
-            468165.6552984868,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            994332.4219424853,
-            468346.6813774005,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            994321.7585003646,
-            470203.04015181964,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "3949": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -5649322.981644171,
-            5568553.523243326,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -5650238.180866389,
-            5569703.215678348,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -5648623.59710415,
-            5571501.099138989,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "4171": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -52.54,
-            4.22269896902571,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.55,
-            4.22438936251509,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.55,
-            4.24113309117193,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "4230": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -52.54,
-            4.22269896902571,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.55,
-            4.22438936251509,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.55,
-            4.24113309117193,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "4275": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -52.5384701246372,
-            4.220239072221915,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.548470035581744,
-            4.221929661220853,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.54847000271151,
-            4.238675131063746,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "4326": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -52.54,
-            4.22269896902571,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.55,
-            4.22438936251509,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.55,
-            4.24113309117193,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "4467": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            995446.5942225576,
-            468165.6552984868,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            994332.4219424853,
-            468346.6813774005,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            994321.7585003646,
-            470203.04015181964,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "4471": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -16143544.355460875,
-            26292802.745847106,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -16136616.09490179,
-            26296480.29590045,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -16128766.302407652,
-            26286263.400504485,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "4624": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -52.54,
-            4.22269896902571,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.55,
-            4.22438936251509,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -52.55,
-            4.24113309117193,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "4807": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            -60.97299921255985,
-            4.689154524691016,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -60.98411022472045,
-            4.691032956912059,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            -60.984110188197974,
-            4.709639034515273,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-  "5490": {
-    "features": [
-      {
-        "geometry": {
-          "coordinates": [
-            1667272.1766113704,
-            474656.11258733075,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point 8",
-          "nom": "8",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            1666141.191191891,
-            474830.76446630707,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point W",
-          "nom": "W",
-        },
-        "type": "Feature",
-      },
-      {
-        "geometry": {
-          "coordinates": [
-            1666115.5675549011,
-            476712.6310222301,
-          ],
-          "type": "Point",
-        },
-        "properties": {
-          "description": "description point *",
-          "nom": "*",
-        },
-        "type": "Feature",
-      },
-    ],
-    "type": "FeatureCollection",
-  },
-}
-`;
-
 exports[`geojsonImport > csv valide avec des virgules 1`] = `
 {
   "communes": [],
diff --git a/packages/api/src/api/rest/administrations.queries.ts b/packages/api/src/api/rest/administrations.queries.ts
index 5e32f255425a31bfe61ac5fd48b74c8271a66930..ac131b1dc24a642227c9fe64cdee17cb9baa8046 100644
--- a/packages/api/src/api/rest/administrations.queries.ts
+++ b/packages/api/src/api/rest/administrations.queries.ts
@@ -1,6 +1,6 @@
 /* eslint-disable no-restricted-syntax */
 import { Pool } from 'pg'
-import { Redefine, dbQueryAndValidate } from '../../pg-database.js'
+import { DbQueryAccessError, Redefine, dbQueryAndValidate, newDbQueryAndValidate } from '../../pg-database.js'
 import { sql } from '@pgtyped/runtime'
 import { AdministrationId, administrationIdValidator } from 'camino-common/src/static/administrations.js'
 import { AdministrationActiviteTypeEmail, administrationActiviteTypeEmailValidator } from 'camino-common/src/administrations.js'
@@ -15,6 +15,10 @@ import { AdminUserNotNull, adminUserNotNullValidator } from 'camino-common/src/r
 import { ActivitesTypesId } from 'camino-common/src/static/activitesTypes.js'
 import { NonEmptyArray } from 'camino-common/src/typescript-tools.js'
 import { z } from 'zod'
+import TE from 'fp-ts/lib/TaskEither.js'
+import { CaminoError } from 'camino-common/src/zod-tools.js'
+import { ZodUnparseable } from '../../tools/fp-tools.js'
+import { pipe } from 'fp-ts/lib/function.js'
 
 export const getUtilisateursByAdministrationId = async (pool: Pool, administrationId: AdministrationId): Promise<AdminUserNotNull[]> => {
   const result = await dbQueryAndValidate(getUtilisateursByAdministrationIdDb, { administrationId }, pool, getUtilisateursByAdministrationIdDbValidator)
@@ -55,8 +59,15 @@ where
     administration_id = $ administrationId !
 `
 
-export const deleteAdministrationActiviteTypeEmail = async (pool: Pool, administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail): Promise<void> => {
-  await dbQueryAndValidate(deleteAdministrationActiviteTypeEmailDb, { administrationId, ...administrationActiviteTypeEmail }, pool, z.void())
+export const deleteAdministrationActiviteTypeEmail = (
+  pool: Pool,
+  administrationId: AdministrationId,
+  administrationActiviteTypeEmail: AdministrationActiviteTypeEmail
+): TE.TaskEither<CaminoError<ZodUnparseable | DbQueryAccessError>, boolean> => {
+  return pipe(
+    newDbQueryAndValidate(deleteAdministrationActiviteTypeEmailDb, { administrationId, ...administrationActiviteTypeEmail }, pool, z.void()),
+    TE.map(() => true)
+  )
 }
 
 const deleteAdministrationActiviteTypeEmailDb = sql<
@@ -68,8 +79,15 @@ where administration_id = $ administrationId !
     and email = $ email !
 `
 
-export const insertAdministrationActiviteTypeEmail = async (pool: Pool, administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail): Promise<void> => {
-  await dbQueryAndValidate(insertAdministrationActiviteTypeEmailDb, { administrationId, ...administrationActiviteTypeEmail }, pool, z.void())
+export const insertAdministrationActiviteTypeEmail = (
+  pool: Pool,
+  administrationId: AdministrationId,
+  administrationActiviteTypeEmail: AdministrationActiviteTypeEmail
+): TE.TaskEither<CaminoError<ZodUnparseable | DbQueryAccessError>, boolean> => {
+  return pipe(
+    newDbQueryAndValidate(insertAdministrationActiviteTypeEmailDb, { administrationId, ...administrationActiviteTypeEmail }, pool, z.void()),
+    TE.map(() => true)
+  )
 }
 
 const insertAdministrationActiviteTypeEmailDb = sql<
diff --git a/packages/api/src/api/rest/administrations.test.integration.ts b/packages/api/src/api/rest/administrations.test.integration.ts
index 917460289eb41433c7cccc9da349a53d456e6ba1..f89f93763d6763a2c63c8a485660526c16a2c499 100644
--- a/packages/api/src/api/rest/administrations.test.integration.ts
+++ b/packages/api/src/api/rest/administrations.test.integration.ts
@@ -1,4 +1,4 @@
-import { restCall, restPostCall, userGenerate } from '../../../tests/_utils/index.js'
+import { restCall, restNewPostCall, userGenerate } from '../../../tests/_utils/index.js'
 import { dbManager } from '../../../tests/db-manager.js'
 import { expect, test, describe, afterAll, beforeAll, vi } from 'vitest'
 import type { Pool } from 'pg'
@@ -93,13 +93,13 @@ describe('administrationActiviteTypeEmails', () => {
       expect(tested.body).toEqual([])
 
       const newActiviteTypeEmail: AdministrationActiviteTypeEmail = { activite_type_id: 'gra', email: 'toto@toto.com' }
-      await restPostCall(dbPool, '/rest/administrations/:administrationId/activiteTypeEmails', { administrationId: 'dea-guyane-01' }, user, newActiviteTypeEmail)
+      await restNewPostCall(dbPool, '/rest/administrations/:administrationId/activiteTypeEmails', { administrationId: 'dea-guyane-01' }, user, newActiviteTypeEmail)
 
       tested = await restCall(dbPool, '/rest/administrations/:administrationId/activiteTypeEmails', { administrationId: 'dea-guyane-01' }, user)
       expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
       expect(tested.body).toEqual([newActiviteTypeEmail])
 
-      await restPostCall(dbPool, '/rest/administrations/:administrationId/activiteTypeEmails/delete', { administrationId: 'dea-guyane-01' }, user, newActiviteTypeEmail)
+      await restNewPostCall(dbPool, '/rest/administrations/:administrationId/activiteTypeEmails/delete', { administrationId: 'dea-guyane-01' }, user, newActiviteTypeEmail)
       tested = await restCall(dbPool, '/rest/administrations/:administrationId/activiteTypeEmails', { administrationId: 'dea-guyane-01' }, user)
       expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
       expect(tested.body).toEqual([])
diff --git a/packages/api/src/api/rest/administrations.ts b/packages/api/src/api/rest/administrations.ts
index 9295666c896e67c80f4e2dd341b5ea9ff180dc7d..3ac6ec07be9d5b13b2f700979efb18bb31d2c8f7 100644
--- a/packages/api/src/api/rest/administrations.ts
+++ b/packages/api/src/api/rest/administrations.ts
@@ -1,13 +1,19 @@
 import { Request as JWTRequest } from 'express-jwt'
 import { HTTP_STATUS } from 'camino-common/src/http.js'
 
+import TE from 'fp-ts/lib/TaskEither.js'
 import { CustomResponse } from './express-type.js'
-import { AdminUserNotNull, User } from 'camino-common/src/roles.js'
+import { AdminUserNotNull, User, UserNotNull } from 'camino-common/src/roles.js'
 import { Pool } from 'pg'
-import { administrationIdValidator } from 'camino-common/src/static/administrations.js'
+import { AdministrationId, administrationIdValidator } from 'camino-common/src/static/administrations.js'
 import { canReadAdministrations } from 'camino-common/src/permissions/administrations.js'
 import { deleteAdministrationActiviteTypeEmail, getActiviteTypeEmailsByAdministrationId, getUtilisateursByAdministrationId, insertAdministrationActiviteTypeEmail } from './administrations.queries.js'
-import { AdministrationActiviteTypeEmail, administrationActiviteTypeEmailValidator } from 'camino-common/src/administrations.js'
+import { AdministrationActiviteTypeEmail } from 'camino-common/src/administrations.js'
+import { CaminoApiError } from '../../types.js'
+import { pipe } from 'fp-ts/lib/function.js'
+import { ZodUnparseable } from '../../tools/fp-tools.js'
+import { DeepReadonly, exhaustiveCheck } from 'camino-common/src/typescript-tools.js'
+import { DbQueryAccessError } from '../../pg-database.js'
 
 export const getAdministrationUtilisateurs = (pool: Pool) => async (req: JWTRequest<User>, res: CustomResponse<AdminUserNotNull[]>) => {
   const user = req.auth
@@ -51,52 +57,62 @@ export const getAdministrationActiviteTypeEmails = (pool: Pool) => async (req: J
   }
 }
 
-export const addAdministrationActiviteTypeEmails = (pool: Pool) => async (req: JWTRequest<User>, res: CustomResponse<boolean>) => {
-  const user = req.auth
-
-  const parsed = administrationIdValidator.safeParse(req.params.administrationId)
-  const bodyParsed = administrationActiviteTypeEmailValidator.safeParse(req.body)
-
-  if (!parsed.success) {
-    console.warn(`l'administrationId est obligatoire`)
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else if (!bodyParsed.success) {
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
-  } else if (!canReadAdministrations(user)) {
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else {
-    try {
-      await insertAdministrationActiviteTypeEmail(pool, parsed.data, bodyParsed.data)
-      res.json(true)
-    } catch (e) {
-      console.error(e)
-
-      res.sendStatus(HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR)
-    }
-  }
+export const addAdministrationActiviteTypeEmails = (
+  pool: Pool,
+  user: DeepReadonly<UserNotNull>,
+  body: DeepReadonly<AdministrationActiviteTypeEmail>,
+  params: { administrationId: AdministrationId }
+): TE.TaskEither<CaminoApiError<'Accès interdit' | ZodUnparseable | DbQueryAccessError>, boolean> => {
+  return pipe(
+    TE.Do,
+    TE.filterOrElseW(
+      () => canReadAdministrations(user),
+      () => ({ message: 'Accès interdit' as const })
+    ),
+    TE.flatMap(() => insertAdministrationActiviteTypeEmail(pool, params.administrationId, body)),
+    TE.mapLeft(caminoError => {
+      const message = caminoError.message
+      switch (message) {
+        case 'Accès interdit':
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_FORBIDDEN }
+        case "Impossible d'accéder à la base de données":
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR }
+        case 'Problème de validation de données':
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_BAD_REQUEST }
+        default:
+          exhaustiveCheck(message)
+          throw new Error('impossible')
+      }
+    })
+  )
 }
 
-export const deleteAdministrationActiviteTypeEmails = (pool: Pool) => async (req: JWTRequest<User>, res: CustomResponse<boolean>) => {
-  const user = req.auth
-
-  const parsed = administrationIdValidator.safeParse(req.params.administrationId)
-  const bodyParsed = administrationActiviteTypeEmailValidator.safeParse(req.body)
-
-  if (!parsed.success) {
-    console.warn(`l'administrationId est obligatoire`)
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else if (!bodyParsed.success) {
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
-  } else if (!canReadAdministrations(user)) {
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else {
-    try {
-      await deleteAdministrationActiviteTypeEmail(pool, parsed.data, bodyParsed.data)
-      res.json(true)
-    } catch (e) {
-      console.error(e)
-
-      res.sendStatus(HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR)
-    }
-  }
+export const deleteAdministrationActiviteTypeEmails = (
+  pool: Pool,
+  user: DeepReadonly<UserNotNull>,
+  body: DeepReadonly<AdministrationActiviteTypeEmail>,
+  params: { administrationId: AdministrationId }
+): TE.TaskEither<CaminoApiError<'Accès interdit' | ZodUnparseable | DbQueryAccessError>, boolean> => {
+  return pipe(
+    TE.Do,
+    TE.filterOrElseW(
+      () => canReadAdministrations(user),
+      () => ({ message: 'Accès interdit' as const })
+    ),
+    TE.flatMap(() => deleteAdministrationActiviteTypeEmail(pool, params.administrationId, body)),
+    TE.mapLeft(caminoError => {
+      const message = caminoError.message
+      switch (message) {
+        case 'Accès interdit':
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_FORBIDDEN }
+        case "Impossible d'accéder à la base de données":
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR }
+        case 'Problème de validation de données':
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_BAD_REQUEST }
+        default:
+          exhaustiveCheck(message)
+          throw new Error('impossible')
+      }
+    })
+  )
 }
diff --git a/packages/api/src/api/rest/entreprises.ts b/packages/api/src/api/rest/entreprises.ts
index b4502c60d3b7d6ab1e9852386967e5a74af8585b..be12d48f7da67de6bce25d46d8358fc5db81c300 100644
--- a/packages/api/src/api/rest/entreprises.ts
+++ b/packages/api/src/api/rest/entreprises.ts
@@ -21,7 +21,7 @@ import Titres from '../../database/models/titres.js'
 import { CustomResponse } from './express-type.js'
 import { SubstanceFiscale, substancesFiscalesBySubstanceLegale } from 'camino-common/src/static/substancesFiscales.js'
 import { Departements, toDepartementId } from 'camino-common/src/static/departement.js'
-import { isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools.js'
+import { DeepReadonly, isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools.js'
 import { Regions } from 'camino-common/src/static/region.js'
 import { anneePrecedente, caminoAnneeToNumber, isAnnee } from 'camino-common/src/date.js'
 import {
@@ -148,7 +148,7 @@ export const bodyBuilder = (
             const surfaceTotale = titre.communes.reduce((value, commune) => value + (commune.surface ?? 0), 0)
 
             let communePrincipale: ICommune | null = null
-            const communes: ICommune[] = titre.communes
+            const communes: DeepReadonly<ICommune[]> = titre.communes
             for (const commune of communes) {
               if (communePrincipale === null) {
                 communePrincipale = commune
diff --git a/packages/api/src/api/rest/etapes.test.integration.ts b/packages/api/src/api/rest/etapes.test.integration.ts
index 39a17fa1dc2c07839a1d2d0eb6e273a1e8b96653..b2568205e82982217970a5989f7a8def67100f38 100644
--- a/packages/api/src/api/rest/etapes.test.integration.ts
+++ b/packages/api/src/api/rest/etapes.test.integration.ts
@@ -34,84 +34,148 @@ afterAll(async () => {
   await dbManager.closeKnex()
 })
 
-test('getEtapesTypesEtapesStatusWithMainStep', async () => {
-  const titre = await titreCreate(
-    {
-      nom: 'nomTitre',
-      typeId: 'arm',
-      titreStatutId: 'val',
-      propsTitreEtapesIds: {},
-    },
-    {}
-  )
+describe('getEtapesTypesEtapesStatusWithMainStep', () => {
+  test('nouvelle étapes possibles', async () => {
+    const titre = await titreCreate(
+      {
+        nom: 'nomTitre',
+        typeId: 'arm',
+        titreStatutId: 'val',
+        propsTitreEtapesIds: {},
+      },
+      {}
+    )
 
-  const titreDemarche = await titreDemarcheCreate({
-    titreId: titre.id,
-    typeId: 'oct',
-  })
+    const titreDemarche = await titreDemarcheCreate({
+      titreId: titre.id,
+      typeId: 'oct',
+    })
 
-  const tested = await restCall(dbPool, '/rest/etapesTypes/:demarcheId/:date', { demarcheId: titreDemarche.id, date: getCurrent() }, userSuper)
+    const tested = await restCall(dbPool, '/rest/etapesTypes/:demarcheId/:date', { demarcheId: titreDemarche.id, date: getCurrent() }, userSuper)
 
-  expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
-  // TODO 2024-06-19 changer ce format ?
-  // Partir plutôt sur un object avec comme clé le etapeTypeId, une liste de etapeStatut associée et la clé mainStep (soit sur le statut, soit directement au top niveau)
-  // soit { mfr: {statuts: ['fai'], mainStep: true}}
-  // soit { mfr: {statuts: [{id: 'fai', mainStep: true}]}}
-  expect(tested.body).toMatchInlineSnapshot(`
-    [
-      {
-        "etapeStatutId": "fai",
-        "etapeTypeId": "mfr",
-        "mainStep": true,
-      },
-      {
-        "etapeStatutId": "fai",
-        "etapeTypeId": "mfr",
-        "mainStep": true,
-      },
-      {
-        "etapeStatutId": "fai",
-        "etapeTypeId": "mfr",
-        "mainStep": true,
-      },
-      {
-        "etapeStatutId": "fai",
-        "etapeTypeId": "mfr",
-        "mainStep": true,
-      },
-      {
-        "etapeStatutId": "fai",
-        "etapeTypeId": "mfr",
-        "mainStep": true,
-      },
-      {
-        "etapeStatutId": "fai",
-        "etapeTypeId": "pfd",
-        "mainStep": true,
-      },
-      {
-        "etapeStatutId": "req",
-        "etapeTypeId": "dae",
-        "mainStep": false,
-      },
-      {
-        "etapeStatutId": "exe",
-        "etapeTypeId": "dae",
-        "mainStep": true,
-      },
+    expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
+    // TODO 2024-06-19 changer ce format ?
+    // Partir plutôt sur un object avec comme clé le etapeTypeId, une liste de etapeStatut associée et la clé mainStep (soit sur le statut, soit directement au top niveau)
+    // soit { mfr: {statuts: ['fai'], mainStep: true}}
+    // soit { mfr: {statuts: [{id: 'fai', mainStep: true}]}}
+    expect(tested.body).toMatchInlineSnapshot(`
+      [
+        {
+          "etapeStatutId": "fai",
+          "etapeTypeId": "mfr",
+          "mainStep": true,
+        },
+        {
+          "etapeStatutId": "fai",
+          "etapeTypeId": "mfr",
+          "mainStep": true,
+        },
+        {
+          "etapeStatutId": "fai",
+          "etapeTypeId": "mfr",
+          "mainStep": true,
+        },
+        {
+          "etapeStatutId": "fai",
+          "etapeTypeId": "mfr",
+          "mainStep": true,
+        },
+        {
+          "etapeStatutId": "fai",
+          "etapeTypeId": "mfr",
+          "mainStep": true,
+        },
+        {
+          "etapeStatutId": "fai",
+          "etapeTypeId": "pfd",
+          "mainStep": true,
+        },
+        {
+          "etapeStatutId": "req",
+          "etapeTypeId": "dae",
+          "mainStep": false,
+        },
+        {
+          "etapeStatutId": "exe",
+          "etapeTypeId": "dae",
+          "mainStep": true,
+        },
+        {
+          "etapeStatutId": "def",
+          "etapeTypeId": "rde",
+          "mainStep": false,
+        },
+        {
+          "etapeStatutId": "fav",
+          "etapeTypeId": "rde",
+          "mainStep": true,
+        },
+      ]
+    `)
+  })
+  test('nouvelle étapes possibles prends en compte les brouillons', async () => {
+    const titre = await titreCreate(
       {
-        "etapeStatutId": "def",
-        "etapeTypeId": "rde",
-        "mainStep": false,
+        nom: 'nomTitre',
+        typeId: 'arm',
+        titreStatutId: 'val',
+        propsTitreEtapesIds: {},
       },
+      {}
+    )
+
+    const titreDemarche = await titreDemarcheCreate({
+      titreId: titre.id,
+      typeId: 'oct',
+    })
+
+    await titreEtapeCreate(
       {
-        "etapeStatutId": "fav",
-        "etapeTypeId": "rde",
-        "mainStep": true,
+        typeId: 'mfr',
+        date: toCaminoDate('2024-06-27'),
+        titreDemarcheId: titreDemarche.id,
+        statutId: 'fai',
+        isBrouillon: ETAPE_IS_BROUILLON,
       },
-    ]
-  `)
+      userSuper,
+      titre.id
+    )
+
+    const tested = await restCall(dbPool, '/rest/etapesTypes/:demarcheId/:date', { demarcheId: titreDemarche.id, date: getCurrent() }, userSuper)
+
+    expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
+    expect(tested.body).toMatchInlineSnapshot(`
+        [
+          {
+            "etapeStatutId": "fai",
+            "etapeTypeId": "pfd",
+            "mainStep": true,
+          },
+          {
+            "etapeStatutId": "req",
+            "etapeTypeId": "dae",
+            "mainStep": false,
+          },
+          {
+            "etapeStatutId": "exe",
+            "etapeTypeId": "dae",
+            "mainStep": true,
+          },
+          {
+            "etapeStatutId": "def",
+            "etapeTypeId": "rde",
+            "mainStep": false,
+          },
+          {
+            "etapeStatutId": "fav",
+            "etapeTypeId": "rde",
+            "mainStep": true,
+          },
+        ]
+      `)
+  })
 })
+
 describe('etapeSupprimer', () => {
   test.each([undefined, 'admin' as Role])('ne peut pas supprimer une étape (utilisateur %s)', async (role: Role | undefined) => {
     const titre = await titreCreate(
diff --git a/packages/api/src/api/rest/etapes.ts b/packages/api/src/api/rest/etapes.ts
index 2327261c93d369a79e034681737e8e0800151942..5ed1aa081a94c04567c51f8b17a17bad5643dfe9 100644
--- a/packages/api/src/api/rest/etapes.ts
+++ b/packages/api/src/api/rest/etapes.ts
@@ -77,6 +77,7 @@ import { FeatureMultiPolygon, FeatureCollectionPoints } from 'camino-common/src/
 import { canHaveForages } from 'camino-common/src/permissions/titres.js'
 import { SecteursMaritimes, getSecteurMaritime } from 'camino-common/src/static/facades.js'
 import { GEO_SYSTEME_IDS } from 'camino-common/src/static/geoSystemes.js'
+import { isRight } from 'fp-ts/lib/Either.js'
 
 export const getEtapeEntrepriseDocuments =
   (pool: Pool) =>
@@ -381,9 +382,11 @@ const getForagesProperties = async (
   pool: Pool
 ): Promise<Pick<GraphqlEtape, 'geojson4326Forages' | 'geojsonOrigineForages'>> => {
   if (canHaveForages(titreTypeId) && isNotNullNorUndefined(geojsonOrigineForages) && isNotNullNorUndefined(geojsonOrigineGeoSystemeId)) {
-    return {
-      geojson4326Forages: await convertPoints(pool, geojsonOrigineGeoSystemeId, GEO_SYSTEME_IDS.WGS84, geojsonOrigineForages),
-      geojsonOrigineForages,
+    const conversion = await convertPoints(pool, geojsonOrigineGeoSystemeId, GEO_SYSTEME_IDS.WGS84, geojsonOrigineForages)()
+    if (isRight(conversion)) {
+      return { geojson4326Forages: conversion.right, geojsonOrigineForages }
+    } else {
+      throw new Error(conversion.left.message)
     }
   }
 
@@ -394,7 +397,7 @@ const getForagesProperties = async (
 }
 type PerimetreInfos = {
   secteursMaritime: SecteursMaritimes[]
-  sdomZones: SDOMZoneId[]
+  sdomZones: DeepReadonly<SDOMZoneId[]>
   surface: KM2 | null
 } & Pick<GraphqlEtape, 'geojson4326Forages' | 'geojsonOrigineForages'> &
   Pick<GetGeojsonInformation, 'communes' | 'forets'>
@@ -413,17 +416,22 @@ const getPerimetreInfosInternal = async (
         throw new Error(`les points doivent être sur le périmètre`)
       }
     }
-    const { communes, sdom, surface, forets, secteurs } = await getGeojsonInformation(pool, geojson4326Perimetre.geometry)
-    const { geojson4326Forages } = await getForagesProperties(titreTypeId, geojsonOrigineGeoSystemeId, geojsonOrigineForages, pool)
+    const result = await getGeojsonInformation(pool, geojson4326Perimetre.geometry)()
+    if (isRight(result)) {
+      const { communes, sdom, surface, forets, secteurs } = result.right
+      const { geojson4326Forages } = await getForagesProperties(titreTypeId, geojsonOrigineGeoSystemeId, geojsonOrigineForages, pool)
 
-    return {
-      surface,
-      communes,
-      forets,
-      secteursMaritime: secteurs.map(s => getSecteurMaritime(s)),
-      sdomZones: sdom,
-      geojson4326Forages,
-      geojsonOrigineForages,
+      return {
+        surface,
+        communes,
+        forets,
+        secteursMaritime: secteurs.map(s => getSecteurMaritime(s)),
+        sdomZones: sdom,
+        geojson4326Forages,
+        geojsonOrigineForages,
+      }
+    } else {
+      throw new Error(result.left.message)
     }
   } else {
     return {
@@ -881,10 +889,15 @@ export const deposeEtape = (pool: Pool) => async (req: CaminoRequest, res: Custo
       const sdomZones: SDOMZoneId[] = []
       const communes: CommuneId[] = []
       if (isNotNullNorUndefined(titreEtape.geojson4326Perimetre)) {
-        const { sdom, communes: communeFromGeoJson } = await getGeojsonInformation(pool, titreEtape.geojson4326Perimetre.geometry)
+        const result = await getGeojsonInformation(pool, titreEtape.geojson4326Perimetre.geometry)()
+        if (isRight(result)) {
+          const { sdom, communes: communeFromGeoJson } = result.right
 
-        communes.push(...communeFromGeoJson.map(({ id }) => id))
-        sdomZones.push(...sdom)
+          communes.push(...communeFromGeoJson.map(({ id }) => id))
+          sdomZones.push(...sdom)
+        } else {
+          throw new Error(result.left.message)
+        }
       }
       const titreTypeId = memoize(() => Promise.resolve(titre.typeId))
       const administrationsLocales = memoize(() => Promise.resolve(titre.administrationsLocales ?? []))
@@ -1061,7 +1074,6 @@ const demarcheEtapesTypesGet = async (titreDemarcheId: DemarcheId, date: CaminoD
     etapesTypes.push(...etapesTypesTDE.flatMap(etapeTypeId => getEtapesStatuts(etapeTypeId).map(etapeStatut => ({ etapeTypeId, etapeStatutId: etapeStatut.id, mainStep: false }))))
   }
 
-  // FIXME integration tests
   // On ne peut pas avoir 2 fois le même type d'étape en brouillon
   const etapeTypeIdInBrouillon = titreDemarche.etapes?.filter(({ isBrouillon, id }) => id !== titreEtapeId && isBrouillon).map(({ typeId }) => typeId) ?? []
 
diff --git a/packages/api/src/api/rest/logs.queries.ts b/packages/api/src/api/rest/logs.queries.ts
index 56b3a1ea827af911b9f13a667f932ccae2a82e68..817843b9bf9be43143af426754b68bc33399378f 100644
--- a/packages/api/src/api/rest/logs.queries.ts
+++ b/packages/api/src/api/rest/logs.queries.ts
@@ -1,13 +1,16 @@
 /* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
-import { Redefine, dbQueryAndValidate } from '../../pg-database.js'
+import { DbQueryAccessError, Redefine, newDbQueryAndValidate } from '../../pg-database.js'
 import { Pool } from 'pg'
 import { z } from 'zod'
 import { IInsertLogInternalQuery } from './logs.queries.types.js'
 import { UtilisateurId } from 'camino-common/src/roles.js'
+import { TaskEither } from 'fp-ts/lib/TaskEither.js'
+import { CaminoError } from 'camino-common/src/zod-tools.js'
+import { ZodUnparseable } from '../../tools/fp-tools.js'
 
-export const addLog = async (pool: Pool, utilisateur_id: UtilisateurId, method: string, path: string, body?: unknown) =>
-  dbQueryAndValidate(insertLogInternal, { utilisateur_id, method, path, body }, pool, z.void())
+export const addLog = (pool: Pool, utilisateur_id: UtilisateurId, method: string, path: string, body?: unknown): TaskEither<CaminoError<ZodUnparseable | DbQueryAccessError>, void[]> =>
+  newDbQueryAndValidate(insertLogInternal, { utilisateur_id, method, path, body }, pool, z.void())
 
 const insertLogInternal = sql<
   Redefine<
diff --git a/packages/api/src/api/rest/perimetre.queries.ts b/packages/api/src/api/rest/perimetre.queries.ts
index bcaca71f082284ea6026fb989406afee0011a788..d6660275cbbffd5c94db19a7b4abbc233d0f1257 100644
--- a/packages/api/src/api/rest/perimetre.queries.ts
+++ b/packages/api/src/api/rest/perimetre.queries.ts
@@ -1,7 +1,9 @@
 /* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
-import { Redefine, dbQueryAndValidate } from '../../pg-database.js'
+import { Redefine, newDbQueryAndValidate, DbQueryAccessError } from '../../pg-database.js'
 import { z } from 'zod'
+import TE from 'fp-ts/lib/TaskEither.js'
+import E from 'fp-ts/lib/Either.js'
 import { Pool } from 'pg'
 import { GeoSystemeId } from 'camino-common/src/static/geoSystemes.js'
 import { FeatureMultiPolygon, GenericFeatureCollection, MultiPoint, MultiPolygon, featureMultiPolygonValidator, multiPointsValidator, multiPolygonValidator } from 'camino-common/src/perimetre.js'
@@ -13,29 +15,56 @@ import { communeIdValidator } from 'camino-common/src/static/communes.js'
 import { secteurDbIdValidator } from 'camino-common/src/static/facades.js'
 import { foretIdValidator } from 'camino-common/src/static/forets.js'
 import { sdomZoneIdValidator } from 'camino-common/src/static/sdom.js'
-import { KM2, km2Validator, m2Validator } from 'camino-common/src/number.js'
-import { isNullOrUndefined } from 'camino-common/src/typescript-tools.js'
+import { KM2, M2, createM2Validator, km2Validator, m2Validator } from 'camino-common/src/number.js'
+import { DeepReadonly, isNullOrUndefined } from 'camino-common/src/typescript-tools.js'
+import { pipe } from 'fp-ts/lib/function.js'
+import { ZodUnparseable, zodParseTaskEither, zodParseTaskEitherCallback } from '../../tools/fp-tools.js'
+import { CaminoError } from 'camino-common/src/zod-tools.js'
 
-export const convertPoints = async <T extends z.ZodTypeAny>(
+const convertPointsStringifyError = 'Impossible de transformer la feature collection' as const
+const convertPointsConversionError = 'La liste des points est vide' as const
+const convertPointsInvalidNumberOfFeaturesError = 'Le nombre de points est invalide' as const
+export type ConvertPointsErrors = DbQueryAccessError | ZodUnparseable | typeof convertPointsStringifyError | typeof convertPointsConversionError | typeof convertPointsInvalidNumberOfFeaturesError
+export const convertPoints = <T extends z.ZodTypeAny>(
   pool: Pool,
   fromGeoSystemeId: GeoSystemeId,
   toGeoSystemeId: GeoSystemeId,
   geojsonPoints: GenericFeatureCollection<T>
-): Promise<GenericFeatureCollection<T>> => {
+): TE.TaskEither<CaminoError<ConvertPointsErrors>, GenericFeatureCollection<T>> => {
   if (fromGeoSystemeId === toGeoSystemeId) {
-    return geojsonPoints
+    return TE.right(geojsonPoints)
   }
 
   const multiPoint: MultiPoint = { type: 'MultiPoint', coordinates: geojsonPoints.features.map(feature => feature.geometry.coordinates) }
 
-  const result = await dbQueryAndValidate(convertMultiPointDb, { fromGeoSystemeId, toGeoSystemeId, geojson: JSON.stringify(multiPoint) }, pool, z.object({ geojson: multiPointsValidator }))
+  return pipe(
+    TE.fromEither(
+      E.tryCatch(
+        () => JSON.stringify(multiPoint),
+        e => ({ message: convertPointsStringifyError, extra: e })
+      )
+    ),
+    TE.flatMap(geojson => newDbQueryAndValidate(convertMultiPointDb, { fromGeoSystemeId, toGeoSystemeId, geojson }, pool, z.object({ geojson: multiPointsValidator }))),
+    TE.flatMap(result => {
+      if (result.length === 0) {
+        return TE.left({ message: convertPointsConversionError })
+      }
 
-  return {
-    type: 'FeatureCollection',
-    features: geojsonPoints.features.map((feature, index) => {
-      return { ...feature, geometry: { type: 'Point', coordinates: result[0].geojson.coordinates[index] } }
+      return TE.right(result[0].geojson.coordinates)
     }),
-  }
+    TE.filterOrElseW(
+      coordinates => coordinates.length === geojsonPoints.features.length,
+      () => ({ message: convertPointsInvalidNumberOfFeaturesError })
+    ),
+    TE.map(coordinates => {
+      return {
+        type: 'FeatureCollection',
+        features: geojsonPoints.features.map((feature, index) => {
+          return { ...feature, geometry: { type: 'Point', coordinates: coordinates[index] } }
+        }),
+      }
+    })
+  )
 }
 
 const convertMultiPointDb = sql<Redefine<IConvertMultiPointDbQuery, { fromGeoSystemeId: GeoSystemeId; toGeoSystemeId: GeoSystemeId; geojson: string }, { geojson: MultiPoint }>>`
@@ -43,27 +72,47 @@ select
     ST_AsGeoJSON (ST_Transform (ST_MAKEVALID (ST_SetSRID (ST_GeomFromGeoJSON ($ geojson !::text), $ fromGeoSystemeId !::integer)), $ toGeoSystemeId !::integer), 40)::json as geojson
 LIMIT 1
 `
-
+const conversionSystemeError = 'Impossible de convertir le geojson vers le système' as const
+const perimetreInvalideError = "Le périmètre n'est pas valide dans le référentiel donné" as const
+const conversionGeometrieError = 'Impossible de convertir la géométrie en JSON' as const
 const getGeojsonByGeoSystemeIdValidator = z.object({ geojson: multiPolygonValidator, is_valid: z.boolean().nullable() })
-export const getGeojsonByGeoSystemeId = async (pool: Pool, fromGeoSystemeId: GeoSystemeId, toGeoSystemeId: GeoSystemeId, geojson: FeatureMultiPolygon): Promise<FeatureMultiPolygon> => {
-  const result = await dbQueryAndValidate(getGeojsonByGeoSystemeIdDb, { fromGeoSystemeId, toGeoSystemeId, geojson: JSON.stringify(geojson.geometry) }, pool, getGeojsonByGeoSystemeIdValidator)
-
-  if (result.length === 1) {
-    if (result[0].is_valid !== true) {
-      throw new Error(`Le périmètre n'est pas valide dans le référentiel '${fromGeoSystemeId}' ${geojson}`)
-    }
-    if (fromGeoSystemeId === toGeoSystemeId) {
-      return geojson
-    }
-    const feature: FeatureMultiPolygon = {
-      type: 'Feature',
-      properties: {},
-      geometry: result[0].geojson,
-    }
-
-    return featureMultiPolygonValidator.parse(feature)
-  }
-  throw new Error(`Impossible de convertir le geojson vers le systeme ${toGeoSystemeId}`)
+export type GetGeojsonByGeoSystemeIdErrorMessages = ZodUnparseable | DbQueryAccessError | typeof conversionSystemeError | typeof perimetreInvalideError | typeof conversionGeometrieError
+export const getGeojsonByGeoSystemeId = (
+  pool: Pool,
+  fromGeoSystemeId: GeoSystemeId,
+  toGeoSystemeId: GeoSystemeId,
+  geojson: FeatureMultiPolygon
+): TE.TaskEither<CaminoError<GetGeojsonByGeoSystemeIdErrorMessages>, FeatureMultiPolygon> => {
+  return pipe(
+    TE.fromEither(
+      E.tryCatch(
+        () => JSON.stringify(geojson.geometry),
+        () => ({ message: conversionGeometrieError })
+      )
+    ),
+    TE.flatMap(geojson => newDbQueryAndValidate(getGeojsonByGeoSystemeIdDb, { fromGeoSystemeId, toGeoSystemeId, geojson }, pool, getGeojsonByGeoSystemeIdValidator)),
+    TE.filterOrElseW(
+      result => result.length === 1,
+      () => ({ message: conversionSystemeError, extra: toGeoSystemeId })
+    ),
+    TE.filterOrElseW(
+      result => result[0].is_valid === true,
+      () => ({ message: perimetreInvalideError, extra: { fromGeoSystemeId, geojson } })
+    ),
+    TE.map(result => {
+      if (fromGeoSystemeId === toGeoSystemeId) {
+        return geojson
+      }
+      const feature: FeatureMultiPolygon = {
+        type: 'Feature',
+        properties: {},
+        geometry: result[0].geojson,
+      }
+
+      return feature
+    }),
+    TE.flatMap(zodParseTaskEitherCallback(featureMultiPolygonValidator))
+  )
 }
 
 const getGeojsonByGeoSystemeIdDb = sql<
@@ -81,9 +130,13 @@ const getTitresIntersectionWithGeojsonValidator = z.object({
   titre_statut_id: titreStatutIdValidator,
 })
 
-type GetTitresIntersectionWithGeojson = z.infer<typeof getTitresIntersectionWithGeojsonValidator>
-export const getTitresIntersectionWithGeojson = async (pool: Pool, geojson4326_perimetre: MultiPolygon, titreSlug: TitreSlug) => {
-  return dbQueryAndValidate(
+export type GetTitresIntersectionWithGeojson = z.infer<typeof getTitresIntersectionWithGeojsonValidator>
+export const getTitresIntersectionWithGeojson = (
+  pool: Pool,
+  geojson4326_perimetre: MultiPolygon,
+  titreSlug: TitreSlug
+): TE.TaskEither<CaminoError<ZodUnparseable | DbQueryAccessError>, GetTitresIntersectionWithGeojson[]> => {
+  return newDbQueryAndValidate(
     getTitresIntersectionWithGeojsonDb,
     {
       titre_slug: titreSlug,
@@ -120,15 +173,19 @@ where
         1) = $ domaine_id !
 `
 
-const numberTokm2 = (value: number): KM2 => km2Validator.parse(Number.parseFloat((value / 1_000_000).toFixed(2)))
+const m2ToKm2 = (value: M2): TE.TaskEither<CaminoError<ZodUnparseable>, KM2> => zodParseTaskEither(km2Validator, Number.parseFloat((value / 1_000_000).toFixed(2)))
 
-export const getGeojsonInformation = async (pool: Pool, geojson4326_perimetre: MultiPolygon): Promise<GetGeojsonInformation> => {
-  const result = await dbQueryAndValidate(getGeojsonInformationDb, { geojson4326_perimetre }, pool, getGeojsonInformationDbValidator)
-  if (result.length !== 1) {
-    throw new Error('On veut un seul résultat')
-  }
-
-  return { ...result[0], surface: numberTokm2(result[0].surface), communes: result[0].communes.map(commune => ({ ...commune, surface: m2Validator.parse(commune.surface) })) }
+const requestError = 'Une erreur inattendue est survenue lors de la récupération des informations geojson en base' as const
+export type GetGeojsonInformationErrorMessages = ZodUnparseable | DbQueryAccessError | typeof requestError
+export const getGeojsonInformation = (pool: Pool, geojson4326_perimetre: MultiPolygon): TE.TaskEither<CaminoError<GetGeojsonInformationErrorMessages>, GetGeojsonInformation> => {
+  return pipe(
+    newDbQueryAndValidate(getGeojsonInformationDb, { geojson4326_perimetre }, pool, getGeojsonInformationDbValidator),
+    TE.bindW('response', result => (result.length === 1 ? TE.right(result[0]) : TE.left({ message: requestError }))),
+    TE.bindW('surface', result => m2ToKm2(result.response.surface)),
+    TE.map(({ response, surface }) => {
+      return { ...response, surface }
+    })
+  )
 }
 
 const nullToEmptyArray = <Y>(val: null | Y[]): Y[] => {
@@ -151,15 +208,14 @@ const getGeojsonInformationValidator = z.object({
 
 // Surface maximale acceptée pour un titre
 const SURFACE_M2_MAX = 100_000 * 1_000_000
-export type GetGeojsonInformation = z.infer<typeof getGeojsonInformationValidator>
+export type GetGeojsonInformation = DeepReadonly<z.infer<typeof getGeojsonInformationValidator>>
+const getGeojsonInformationCommuneDbValidator = z.object({ id: communeIdValidator, nom: z.string(), surface: m2Validator })
+
 const getGeojsonInformationDbValidator = z.object({
-  surface: z.number().max(SURFACE_M2_MAX),
+  surface: createM2Validator(z.number().max(SURFACE_M2_MAX, `Le périmètre ne doit pas excéder ${SURFACE_M2_MAX}M²`)),
   sdom: z.array(sdomZoneIdValidator).nullable().transform(nullToEmptyArray),
   forets: z.array(foretIdValidator).nullable().transform(nullToEmptyArray),
-  communes: z
-    .array(z.object({ id: communeIdValidator, nom: z.string(), surface: z.number() }))
-    .nullable()
-    .transform(nullToEmptyArray),
+  communes: z.array(getGeojsonInformationCommuneDbValidator).nullable().transform(nullToEmptyArray),
   secteurs: z.array(secteurDbIdValidator).nullable().transform(nullToEmptyArray),
 })
 type GetGeojsonInformationDbValidator = z.infer<typeof getGeojsonInformationDbValidator>
diff --git a/packages/api/src/api/rest/perimetre.test.integration.ts b/packages/api/src/api/rest/perimetre.test.integration.ts
index 9f71978761fc62383de4fdbfb993865b7a24a122..f0e6ac2fd26e32014a3ff2c77a54e78f9278e1a7 100644
--- a/packages/api/src/api/rest/perimetre.test.integration.ts
+++ b/packages/api/src/api/rest/perimetre.test.integration.ts
@@ -1,6 +1,6 @@
 import { userSuper } from '../../database/user-super.js'
 import { dbManager } from '../../../tests/db-manager.js'
-import { restPostCall } from '../../../tests/_utils/index.js'
+import { restNewPostCall } from '../../../tests/_utils/index.js'
 import { test, expect, vi, beforeAll, afterAll, describe } from 'vitest'
 import type { Pool } from 'pg'
 import { HTTP_STATUS } from 'camino-common/src/http.js'
@@ -8,14 +8,13 @@ import {
   FeatureCollection,
   FeatureCollectionForages,
   FeatureCollectionPoints,
-  featureCollectionPointsValidator,
   FeatureCollectionPolygon,
   FeatureMultiPolygon,
   GeojsonImportBody,
   GeojsonImportForagesBody,
   GeojsonImportPointsBody,
 } from 'camino-common/src/perimetre.js'
-import { GEO_SYSTEME_IDS, GeoSystemeId, sortedGeoSystemes } from 'camino-common/src/static/geoSystemes.js'
+import { GEO_SYSTEME_IDS } from 'camino-common/src/static/geoSystemes.js'
 import { idGenerate } from '../../database/models/_format/id-create.js'
 import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs'
 import { tempDocumentNameValidator } from 'camino-common/src/document.js'
@@ -24,6 +23,7 @@ import { join } from 'node:path'
 
 console.info = vi.fn()
 console.error = vi.fn()
+console.warn = vi.fn()
 
 let dbPool: Pool
 
@@ -36,36 +36,6 @@ afterAll(async () => {
   await dbManager.closeKnex()
 })
 
-const points: FeatureCollectionPoints = {
-  type: 'FeatureCollection',
-  features: [
-    { type: 'Feature', properties: { nom: '8', description: 'description point 8' }, geometry: { type: 'Point', coordinates: [-52.54, 4.22269896902571] } },
-    { type: 'Feature', properties: { nom: 'W', description: 'description point W' }, geometry: { type: 'Point', coordinates: [-52.55, 4.22438936251509] } },
-    { type: 'Feature', properties: { nom: '*', description: 'description point *' }, geometry: { type: 'Point', coordinates: [-52.55, 4.24113309117193] } },
-  ],
-}
-
-describe('convertGeojsonPointsToGeoSystemeId', () => {
-  test('pas de conversion 4326', async () => {
-    const tested = await restPostCall(dbPool, '/rest/geojson_points/:geoSystemeId', { geoSystemeId: '4326' }, userSuper, featureCollectionPointsValidator.parse(points))
-
-    expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
-    expect(tested.body).toStrictEqual(points)
-  })
-
-  test('toutes les conversions', async () => {
-    const result: { [key in GeoSystemeId]?: unknown } = {}
-    for (const geoSysteme of sortedGeoSystemes) {
-      const tested = await restPostCall(dbPool, '/rest/geojson_points/:geoSystemeId', { geoSystemeId: geoSysteme.id }, userSuper, featureCollectionPointsValidator.parse(points))
-
-      expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
-      result[geoSysteme.id] = tested.body
-    }
-
-    expect(result).toMatchSnapshot()
-  })
-})
-
 const dir = `${process.cwd()}/files/tmp/`
 describe('geojsonImport', () => {
   test('fichier invalide', async () => {
@@ -79,7 +49,13 @@ describe('geojsonImport', () => {
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Une erreur s'est produite lors de l'ouverture du fichier GeoJSON",
+        "status": 400,
+      }
+    `)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 
@@ -100,11 +76,11 @@ C;Point C;1061421.05579599016346;6865050.211738565005362`
       fileType: 'csv',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchSnapshot()
 
-    const testedWithError = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const testedWithError = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(testedWithError.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 
@@ -143,7 +119,7 @@ B;Point B;1063526.397924559889361;6867885.978687250986695`
       fileType: 'csv',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 
@@ -164,11 +140,11 @@ C;Point éç;-52.55;4.24113309117193`
       fileType: 'csv',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchSnapshot()
 
-    const testedWithError = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
+    const testedWithError = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
     expect(testedWithError.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 
@@ -189,11 +165,11 @@ C;Point éç;-52.55;4.24113309117193`
       fileType: 'csv',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchSnapshot()
 
-    const testedWithError = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
+    const testedWithError = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
     expect(testedWithError.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 
@@ -214,7 +190,7 @@ C;Point éç;-52,55;4,24113309117193`
       fileType: 'csv',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchSnapshot()
   })
@@ -251,7 +227,7 @@ C;Point éç;-52,55;4,24113309117193`
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchInlineSnapshot(`
       {
@@ -394,7 +370,14 @@ C;Point éç;-52,55;4,24113309117193`
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Problème de validation de données",
+        "status": 400,
+        "zodErrorReadableMessage": "Validation error: Le périmètre ne doit pas excéder 100000000000M² at "[0].surface"",
+      }
+    `)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 
@@ -432,7 +415,7 @@ C;Point éç;-52,55;4,24113309117193`
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchInlineSnapshot(`
       {
@@ -580,11 +563,60 @@ C;Point éç;-52,55;4,24113309117193`
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGF93 / Lambert-93'] }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGF93 / Lambert-93'] }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchSnapshot()
   })
 
+  // TODO 2024-06-27 ce test devrait retourner une erreur plus parlante
+  test('fichier valide geojson 2154 erreur import en 4326', async () => {
+    const featureMultipolygon: FeatureCollection = {
+      type: 'FeatureCollection',
+      features: [
+        {
+          type: 'Feature',
+          properties: { id: null },
+          geometry: {
+            type: 'MultiPolygon',
+            coordinates: [
+              [
+                [
+                  [1051195.108314365847036, 6867800.046355471946299],
+                  [1063526.397924559889361, 6867885.978687250986695],
+                  [1061421.05579599016346, 6865050.211738565005362],
+                  [1051452.905309700872749, 6864147.922254892066121],
+                  [1044836.115762767498381, 6866081.399719905108213],
+                  [1047113.322554893908091, 6867928.944853140041232],
+                  [1051195.108314365847036, 6867800.046355471946299],
+                ],
+              ],
+            ],
+          },
+        },
+      ],
+    }
+
+    const fileName = `existing_temp_file_${idGenerate()}.geojson`
+    mkdirSync(dir, { recursive: true })
+    writeFileSync(`${dir}/${fileName}`, JSON.stringify(featureMultipolygon))
+    const body: GeojsonImportBody = {
+      titreSlug: titreSlugValidator.parse('titre-slug'),
+      titreTypeId: 'arm',
+      tempDocumentName: tempDocumentNameValidator.parse(fileName),
+      fileType: 'geojson',
+    }
+
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+
+    expect(tested.body).toMatchInlineSnapshot(`
+        {
+          "message": "Problème de validation de données",
+          "status": 400,
+          "zodErrorReadableMessage": "Validation error: Le périmètre ne doit pas excéder 100000000000M² at "[0].surface"",
+        }
+      `)
+  })
+
   test('geojson geometrie non valide', async () => {
     const feature: FeatureCollection = {
       type: 'FeatureCollection',
@@ -610,29 +642,13 @@ C;Point éç;-52,55;4,24113309117193`
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
     expect(tested.body).toMatchInlineSnapshot(`
       {
-        "issues": [
-          {
-            "code": "too_small",
-            "exact": false,
-            "inclusive": true,
-            "message": "Array must contain at least 3 element(s)",
-            "minimum": 3,
-            "path": [
-              "features",
-              0,
-              "geometry",
-              "coordinates",
-              0,
-              0,
-            ],
-            "type": "array",
-          },
-        ],
-        "name": "ZodError",
+        "message": "Problème de validation de données",
+        "status": 400,
+        "zodErrorReadableMessage": "Validation error: Array must contain at least 3 element(s) at "features[0].geometry.coordinates[0][0]"",
       }
     `)
   })
@@ -648,7 +664,7 @@ C;Point éç;-52,55;4,24113309117193`
       fileType: 'shp',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchInlineSnapshot(`
       {
@@ -790,7 +806,7 @@ C;Point éç;-52,55;4,24113309117193`
       fileType: 'shp',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchInlineSnapshot(`
       {
@@ -920,6 +936,91 @@ C;Point éç;-52,55;4,24113309117193`
       }
     `)
   })
+  test('shapefile valide avec conversion', async () => {
+    const fileName = `existing_temp_file_${idGenerate()}.shp`
+    mkdirSync(dir, { recursive: true })
+    copyFileSync(join(__dirname, 'perimetre-guyane.shp'), `${dir}/${fileName}`)
+    const body: GeojsonImportBody = {
+      titreSlug: titreSlugValidator.parse('mfr'),
+      titreTypeId: 'arm',
+      tempDocumentName: tempDocumentNameValidator.parse(fileName),
+      fileType: 'shp',
+    }
+
+    const tested = await restNewPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
+    // expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
+    expect(tested.body).toMatchInlineSnapshot(`
+        {
+          "communes": [],
+          "foretIds": [],
+          "geojson4326_perimetre": {
+            "geometry": {
+              "coordinates": [
+                [
+                  [
+                    [
+                      -0.06057663245889268,
+                      48.72634716602998,
+                    ],
+                    [
+                      0.009012625831280329,
+                      48.560476879146485,
+                    ],
+                    [
+                      -0.2083346466367236,
+                      48.751132381311415,
+                    ],
+                    [
+                      -0.06057663245889268,
+                      48.72634716602998,
+                    ],
+                  ],
+                ],
+              ],
+              "type": "MultiPolygon",
+            },
+            "properties": {},
+            "type": "Feature",
+          },
+          "geojson4326_points": null,
+          "geojson_origine_geo_systeme_id": "2972",
+          "geojson_origine_perimetre": {
+            "geometry": {
+              "coordinates": [
+                [
+                  [
+                    [
+                      4113587.1064621,
+                      6772028.066275262,
+                    ],
+                    [
+                      4132574.3236907134,
+                      6760333.001039797,
+                    ],
+                    [
+                      4102127.2736782786,
+                      6765797.543152926,
+                    ],
+                    [
+                      4113587.1064621,
+                      6772028.066275262,
+                    ],
+                  ],
+                ],
+              ],
+              "type": "MultiPolygon",
+            },
+            "properties": {},
+            "type": "Feature",
+          },
+          "geojson_origine_points": null,
+          "sdomZoneIds": [],
+          "secteurMaritimeIds": [],
+          "superposition_alertes": [],
+          "surface": 93.09,
+        }
+      `)
+  })
 })
 
 describe('geojsonImportPoints', () => {
@@ -931,8 +1032,14 @@ describe('geojsonImportPoints', () => {
       tempDocumentName: tempDocumentNameValidator.parse(fileName),
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_points/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_points/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Fichier incorrect",
+        "status": 400,
+      }
+    `)
   })
 
   test('fichier valide sans conversion', async () => {
@@ -947,7 +1054,7 @@ describe('geojsonImportPoints', () => {
       tempDocumentName: tempDocumentNameValidator.parse(fileName),
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_points/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_points/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchInlineSnapshot(`
       {
@@ -1007,7 +1114,7 @@ describe('geojsonImportPoints', () => {
       tempDocumentName: tempDocumentNameValidator.parse(fileName),
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_points/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGF93 / Lambert-93'] }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_points/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGF93 / Lambert-93'] }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchInlineSnapshot(`
       {
@@ -1052,94 +1159,8 @@ describe('geojsonImportPoints', () => {
       }
     `)
   })
-
-  test('shapefile valide avec conversion', async () => {
-    const fileName = `existing_temp_file_${idGenerate()}.shp`
-    mkdirSync(dir, { recursive: true })
-    copyFileSync(join(__dirname, 'perimetre-guyane.shp'), `${dir}/${fileName}`)
-    const body: GeojsonImportBody = {
-      titreSlug: titreSlugValidator.parse('mfr'),
-      titreTypeId: 'arm',
-      tempDocumentName: tempDocumentNameValidator.parse(fileName),
-      fileType: 'shp',
-    }
-
-    const tested = await restPostCall(dbPool, '/rest/geojson/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
-    expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
-    expect(tested.body).toMatchInlineSnapshot(`
-      {
-        "communes": [],
-        "foretIds": [],
-        "geojson4326_perimetre": {
-          "geometry": {
-            "coordinates": [
-              [
-                [
-                  [
-                    -0.06057663245889268,
-                    48.72634716602998,
-                  ],
-                  [
-                    0.009012625831280329,
-                    48.560476879146485,
-                  ],
-                  [
-                    -0.2083346466367236,
-                    48.751132381311415,
-                  ],
-                  [
-                    -0.06057663245889268,
-                    48.72634716602998,
-                  ],
-                ],
-              ],
-            ],
-            "type": "MultiPolygon",
-          },
-          "properties": {},
-          "type": "Feature",
-        },
-        "geojson4326_points": null,
-        "geojson_origine_geo_systeme_id": "2972",
-        "geojson_origine_perimetre": {
-          "geometry": {
-            "coordinates": [
-              [
-                [
-                  [
-                    4113587.1064621,
-                    6772028.066275262,
-                  ],
-                  [
-                    4132574.3236907134,
-                    6760333.001039797,
-                  ],
-                  [
-                    4102127.2736782786,
-                    6765797.543152926,
-                  ],
-                  [
-                    4113587.1064621,
-                    6772028.066275262,
-                  ],
-                ],
-              ],
-            ],
-            "type": "MultiPolygon",
-          },
-          "properties": {},
-          "type": "Feature",
-        },
-        "geojson_origine_points": null,
-        "sdomZoneIds": [],
-        "secteurMaritimeIds": [],
-        "superposition_alertes": [],
-        "surface": 93.09,
-      }
-    `)
-  })
 })
-
+// TODO 2024-06-27 tester les forages avec des shape
 describe('geojsonImportForages', () => {
   test('fichier geojson invalide', async () => {
     const fileName = `existing_temp_file_${idGenerate()}.geojson`
@@ -1150,7 +1171,13 @@ describe('geojsonImportForages', () => {
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Une erreur s'est produite lors de l'ouverture du fichier GeoJSON",
+        "status": 400,
+      }
+    `)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 
@@ -1173,7 +1200,7 @@ describe('geojsonImportForages', () => {
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchInlineSnapshot(`
       {
@@ -1242,7 +1269,7 @@ describe('geojsonImportForages', () => {
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGF93 / Lambert-93'] }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGF93 / Lambert-93'] }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 
@@ -1265,7 +1292,7 @@ describe('geojsonImportForages', () => {
       fileType: 'geojson',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGF93 / Lambert-93'] }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGF93 / Lambert-93'] }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchInlineSnapshot(`
       {
@@ -1329,11 +1356,11 @@ B;Point B;1063526.397924559889361;6867885.978687250986695;12,12;captage`
       fileType: 'csv',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_OK)
     expect(tested.body).toMatchSnapshot()
 
-    const testedWithError = await restPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
+    const testedWithError = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS.WGS84 }, userSuper, body)
     expect(testedWithError.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 
@@ -1351,7 +1378,15 @@ B;Point B;1063526.397924559889361;6867885.978687250986695;12.12;12,12`
       fileType: 'csv',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
+
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Problème de validation de données",
+        "status": 400,
+        "zodErrorReadableMessage": "Validation error: Invalid enum value. Expected 'captage' | 'rejet' | 'piézomètre', received '1212'",
+      }
+    `)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
   test('csv invalide en 2972 car nom obligatoire', async () => {
@@ -1368,7 +1403,7 @@ B;Point B;1063526.397924559889361;6867885.978687250986695;12.12;captage`
       fileType: 'csv',
     }
 
-    const tested = await restPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
+    const tested = await restNewPostCall(dbPool, '/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId: GEO_SYSTEME_IDS['RGFG95 / UTM zone 22N'] }, userSuper, body)
     expect(tested.statusCode).toBe(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
   })
 })
diff --git a/packages/api/src/api/rest/perimetre.ts b/packages/api/src/api/rest/perimetre.ts
index b8412842cf5354a4b7f4e9ecb34260040c647915..cb294f09b1abc059935505b2cde1032b5451aa83 100644
--- a/packages/api/src/api/rest/perimetre.ts
+++ b/packages/api/src/api/rest/perimetre.ts
@@ -1,12 +1,23 @@
 import { DemarcheId, demarcheIdOrSlugValidator } from 'camino-common/src/demarche.js'
 import { CaminoRequest, CustomResponse } from './express-type.js'
 import { Pool } from 'pg'
-import { GEO_SYSTEME_IDS, GeoSystemes, geoSystemeIdValidator } from 'camino-common/src/static/geoSystemes.js'
+import TE from 'fp-ts/lib/TaskEither.js'
+import { pipe } from 'fp-ts/lib/function.js'
+import { GEO_SYSTEME_IDS, GeoSystemeId, GeoSystemes } from 'camino-common/src/static/geoSystemes.js'
 import { HTTP_STATUS } from 'camino-common/src/http.js'
-import { convertPoints, getGeojsonByGeoSystemeId as getGeojsonByGeoSystemeIdQuery, getGeojsonInformation, getTitresIntersectionWithGeojson } from './perimetre.queries.js'
+import {
+  ConvertPointsErrors,
+  GetGeojsonByGeoSystemeIdErrorMessages,
+  GetGeojsonInformationErrorMessages,
+  GetTitresIntersectionWithGeojson,
+  convertPoints,
+  getGeojsonByGeoSystemeId as getGeojsonByGeoSystemeIdQuery,
+  getGeojsonInformation,
+  getTitresIntersectionWithGeojson,
+} from './perimetre.queries.js'
 import { TitreTypeId } from 'camino-common/src/static/titresTypes.js'
 import { getMostRecentEtapeFondamentaleValide } from './titre-heritage.js'
-import { isAdministrationAdmin, isAdministrationEditeur, isDefault, isSuper, User } from 'camino-common/src/roles.js'
+import { isAdministrationAdmin, isAdministrationEditeur, isDefault, isSuper, User, UserNotNull } from 'camino-common/src/roles.js'
 import { getDemarcheByIdOrSlug, getEtapesByDemarcheId } from './demarches.queries.js'
 import { getAdministrationsLocalesByTitreId, getTitreByIdOrSlug, getTitulairesAmodiatairesByTitreId } from './titres.queries.js'
 import { etapeIdOrSlugValidator } from 'camino-common/src/etape.js'
@@ -19,8 +30,6 @@ import {
   PerimetreInformations,
   featureCollectionPointsValidator,
   featureCollectionMultipolygonValidator,
-  geojsonImportBodyValidator,
-  geojsonImportPointBodyValidator,
   multiPolygonValidator,
   polygonValidator,
   featureCollectionPolygonValidator,
@@ -29,12 +38,14 @@ import {
   featureCollectionForagesValidator,
   FeatureCollectionForages,
   featureForagePropertiesValidator,
-  geojsonImportForagesBodyValidator,
+  GeojsonImportPointsBody,
+  GeojsonImportBody,
+  GeojsonImportForagesBody,
 } from 'camino-common/src/perimetre.js'
 import { join } from 'node:path'
 import { readFileSync } from 'node:fs'
 import shpjs from 'shpjs'
-import { exhaustiveCheck, isNotNullNorUndefined, isNullOrUndefined, memoize } from 'camino-common/src/typescript-tools.js'
+import { DeepReadonly, exhaustiveCheck, isNotNullNorUndefined, isNullOrUndefined, memoize } from 'camino-common/src/typescript-tools.js'
 import { SDOMZoneId } from 'camino-common/src/static/sdom.js'
 import { TitreSlug } from 'camino-common/src/validators/titres.js'
 import { canReadEtape } from './permissions/etapes.js'
@@ -42,22 +53,11 @@ import { EtapeTypeId } from 'camino-common/src/static/etapesTypes.js'
 import xlsx from 'xlsx'
 import { ZodTypeAny, z } from 'zod'
 import { CommuneId } from 'camino-common/src/static/communes'
-
-export const convertGeojsonPointsToGeoSystemeId = (pool: Pool) => async (req: CaminoRequest, res: CustomResponse<FeatureCollectionPoints>) => {
-  const geoSystemeIdParsed = geoSystemeIdValidator.safeParse(req.params.geoSystemeId)
-  const geojsonParsed = featureCollectionPointsValidator.safeParse(req.body)
-
-  if (!geoSystemeIdParsed.success || !geojsonParsed.success) {
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST)
-  } else {
-    try {
-      res.json(await convertPoints(pool, GEO_SYSTEME_IDS.WGS84, geoSystemeIdParsed.data, geojsonParsed.data))
-    } catch (e) {
-      res.sendStatus(HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR)
-      console.error(e)
-    }
-  }
-}
+import { CaminoApiError } from '../../types.js'
+import { DbQueryAccessError } from '../../pg-database.js'
+import E, { isRight } from 'fp-ts/lib/Either.js'
+import { ZodUnparseable, zodParseEither, zodParseEitherCallback } from '../../tools/fp-tools.js'
+import { CaminoError } from 'camino-common/src/zod-tools.js'
 
 export const getPerimetreInfos = (pool: Pool) => async (req: CaminoRequest, res: CustomResponse<PerimetreInformations>) => {
   const user = req.auth
@@ -118,11 +118,16 @@ export const getPerimetreInfos = (pool: Pool) => async (req: CaminoRequest, res:
               { ...demarche, titre_public_lecture: titre.public_lecture }
             )
           ) {
-            res.json({
-              superposition_alertes: await getAlertesSuperposition(etape.geojson4326_perimetre, titre.titre_type_id, titre.titre_slug, user, pool),
-              sdomZoneIds: etape.sdom_zones,
-              communes: etape.communes,
-            })
+            const superpositionAlertes = await getAlertesSuperposition(etape.geojson4326_perimetre, titre.titre_type_id, titre.titre_slug, user, pool)()
+            if (isRight(superpositionAlertes)) {
+              res.json({
+                superposition_alertes: superpositionAlertes.right,
+                sdomZoneIds: etape.sdom_zones,
+                communes: etape.communes,
+              })
+            } else {
+              res.status(HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR).send(superpositionAlertes.left)
+            }
           } else {
             res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
           }
@@ -135,9 +140,6 @@ export const getPerimetreInfos = (pool: Pool) => async (req: CaminoRequest, res:
   }
 }
 
-const tuple4326CoordinateValidator = z.tuple([z.number().min(-180).max(180), z.number().min(-90).max(90)])
-const polygon4326CoordinatesValidator = z.array(z.array(tuple4326CoordinateValidator).min(3)).min(1)
-
 const shapeValidator = z.array(polygonValidator.or(multiPolygonValidator)).max(1).min(1)
 const geojsonValidator = featureCollectionMultipolygonValidator.or(featureCollectionPolygonValidator)
 
@@ -167,297 +169,454 @@ const csvCommonValidator = z
 
 const csvCommonLowerValidator = z.object({ nom: z.string(), description: z.string().optional() })
 const makeCsvToJsonValidator = <T extends ZodTypeAny>(pointValidator: T) => z.array(csvCommonValidator.pipe(pointValidator))
-
-export const geojsonImport = (pool: Pool) => async (req: CaminoRequest, res: CustomResponse<GeojsonInformations>) => {
-  const user = req.auth
-
-  const geoSystemeId = geoSystemeIdValidator.safeParse(req.params.geoSystemeId)
-  if (!user || isDefault(user)) {
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else if (!geoSystemeId.success) {
-    console.warn(`le geoSystemeId est obligatoire`)
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else {
-    const geojsonImportInput = geojsonImportBodyValidator.safeParse(req.body)
-
-    if (geojsonImportInput.success) {
-      try {
-        const filename = geojsonImportInput.data.tempDocumentName
-
-        const pathFrom = join(process.cwd(), `/files/tmp/${filename}`)
-
-        let geojsonOriginFeatureMultiPolygon: FeatureMultiPolygon
-        let geojsonOriginFeatureCollectionPoints: null | FeatureCollectionPoints = null
-        const fileType = geojsonImportInput.data.fileType
-        switch (fileType) {
-          case 'geojson': {
-            const fileContent = readFileSync(pathFrom)
-            const features = geojsonValidator.parse(JSON.parse(fileContent.toString()))
-
-            const firstGeometry = features.features[0].geometry
-            const multiPolygon: MultiPolygon = firstGeometry.type === 'Polygon' ? { type: 'MultiPolygon', coordinates: [firstGeometry.coordinates] } : firstGeometry
-
-            geojsonOriginFeatureMultiPolygon = { type: 'Feature', properties: {}, geometry: multiPolygon }
-
-            // On a des points après le multipolygone
-            if (features.features.length > 1) {
-              const [_multi, ...points] = features.features
-              geojsonOriginFeatureCollectionPoints = { type: 'FeatureCollection', features: points }
-            }
-
-            break
-          }
-
-          case 'shp': {
-            const fileContent = readFileSync(pathFrom)
-            const shpParsed = shpjs.parseShp(fileContent)
-
-            const shapePolygonOrMultipolygon = shapeValidator.parse(shpParsed)[0]
-
-            let coordinates: [number, number][][][]
-            if (shapePolygonOrMultipolygon.type === 'MultiPolygon') {
-              coordinates = shapePolygonOrMultipolygon.coordinates
-            } else {
-              coordinates = [shapePolygonOrMultipolygon.coordinates]
+const csvXYValidator = makeCsvToJsonValidator(csvCommonLowerValidator.extend({ x: csvInputNumberValidator, y: csvInputNumberValidator }))
+const csvLatLongValidator = makeCsvToJsonValidator(csvCommonLowerValidator.extend({ longitude: csvInputNumberValidator, latitude: csvInputNumberValidator }))
+const csvForageXYValidator = makeCsvToJsonValidator(
+  featureForagePropertiesValidator.omit({ profondeur: true }).extend({ x: csvInputNumberValidator, y: csvInputNumberValidator, profondeur: csvInputNumberValidator })
+)
+const csvForageDegValidator = makeCsvToJsonValidator(
+  featureForagePropertiesValidator.omit({ profondeur: true }).extend({ longitude: csvInputNumberValidator, latitude: csvInputNumberValidator, profondeur: csvInputNumberValidator })
+)
+
+const ouvertureGeoJSONError = "Une erreur s'est produite lors de l'ouverture du fichier GeoJSON" as const
+const ouvertureShapeError = "Une erreur s'est produite lors de l'ouverture du fichier shape" as const
+const ouvertureCsvError = 'Une erreur est survenue lors de la lecture du csv' as const
+const importCsvRestrictionError = "L'import CSV est fait pour des petits polygones simple de moins de 20 sommets" as const
+const recuperationInfoCsvError = `Une erreur est survenue lors de la récupération des informations du CSV` as const
+const extractionGeoJSONError = "Une erreur s'est produite lors de l'extraction du multi-polygone du fichier GeoJSON" as const
+const extractionShapeError = "Une erreur s'est produite lors de l'extraction du multi-polygone du fichier shape" as const
+type GeojsonImportErrorMessages =
+  | ZodUnparseable
+  | DbQueryAccessError
+  | typeof ouvertureGeoJSONError
+  | typeof ouvertureShapeError
+  | typeof ouvertureCsvError
+  | typeof importCsvRestrictionError
+  | typeof recuperationInfoCsvError
+  | typeof extractionGeoJSONError
+  | typeof extractionShapeError
+  | GetGeojsonByGeoSystemeIdErrorMessages
+  | GetGeojsonInformationErrorMessages
+  | ConvertPointsErrors
+export const geojsonImport = (
+  pool: Pool,
+  user: DeepReadonly<UserNotNull>,
+  body: DeepReadonly<GeojsonImportBody>,
+  params: { geoSystemeId: GeoSystemeId }
+): TE.TaskEither<CaminoApiError<GeojsonImportErrorMessages>, DeepReadonly<GeojsonInformations>> => {
+  const pathFrom = join(process.cwd(), `/files/tmp/${body.tempDocumentName}`)
+
+  return pipe(
+    TE.fromEither(
+      pipe(
+        E.Do,
+        E.flatMap<
+          Record<string, never>,
+          CaminoError<GeojsonImportErrorMessages>,
+          { geojsonOriginFeatureMultiPolygon: FeatureMultiPolygon; geojsonOriginFeatureCollectionPoints: null | FeatureCollectionPoints }
+        >(() => {
+          const fileType = body.fileType
+          switch (fileType) {
+            case 'geojson': {
+              return pipe(
+                fileNameToJson(pathFrom, geojsonValidator),
+                E.flatMap(features =>
+                  E.tryCatch(
+                    () => {
+                      const firstGeometry = features.features[0].geometry
+                      const multiPolygon: MultiPolygon = firstGeometry.type === 'Polygon' ? { type: 'MultiPolygon', coordinates: [firstGeometry.coordinates] } : firstGeometry
+
+                      const geojsonOriginFeatureMultiPolygon: FeatureMultiPolygon = { type: 'Feature', properties: {}, geometry: multiPolygon }
+
+                      let geojsonOriginFeatureCollectionPoints: null | FeatureCollectionPoints = null
+                      // On a des points après le multipolygone
+                      if (features.features.length > 1) {
+                        const [_multi, ...points] = features.features
+                        geojsonOriginFeatureCollectionPoints = { type: 'FeatureCollection', features: points }
+                      }
+
+                      return { geojsonOriginFeatureMultiPolygon, geojsonOriginFeatureCollectionPoints }
+                    },
+                    e => ({ message: extractionGeoJSONError, extra: e })
+                  )
+                )
+              )
             }
-            geojsonOriginFeatureMultiPolygon = { type: 'Feature', geometry: { type: 'MultiPolygon', coordinates }, properties: {} }
-
-            break
-          }
-
-          case 'csv': {
-            const fileContent = readFileSync(pathFrom, { encoding: 'utf-8' })
-            const result = xlsx.read(fileContent, { type: 'string', FS: ';', raw: true })
-
-            if (result.SheetNames.length !== 1) {
-              throw new Error(`une erreur est survenue lors de la lecture du csv, il ne devrait y avoir qu'un seul document ${result.SheetNames}`)
+            case 'shp': {
+              return pipe(
+                fileNameToShape(pathFrom, shapeValidator),
+                E.flatMap(shapePolygonOrMultipolygons =>
+                  E.tryCatch(
+                    () => {
+                      const shapePolygonOrMultipolygon = shapePolygonOrMultipolygons[0]
+                      let coordinates: [number, number][][][]
+                      if (shapePolygonOrMultipolygon.type === 'MultiPolygon') {
+                        coordinates = shapePolygonOrMultipolygon.coordinates
+                      } else {
+                        coordinates = [shapePolygonOrMultipolygon.coordinates]
+                      }
+                      const geojsonOriginFeatureMultiPolygon: FeatureMultiPolygon = { type: 'Feature', geometry: { type: 'MultiPolygon', coordinates }, properties: {} }
+
+                      return { geojsonOriginFeatureMultiPolygon, geojsonOriginFeatureCollectionPoints: null }
+                    },
+                    e => ({ message: extractionShapeError, extra: e })
+                  )
+                )
+              )
             }
-            const sheet1 = result.Sheets[result.SheetNames[0]]
-            const converted = xlsx.utils.sheet_to_json(sheet1, { raw: true })
-            const uniteId = GeoSystemes[geoSystemeId.data].uniteId
-
-            if (converted.length > 20) {
-              throw new Error(`L'import CSV est fait pour des petits polygones simple de moins de 20 sommets`)
+            case 'csv': {
+              return pipe(
+                fileNameToCsv(pathFrom),
+                E.filterOrElseW(
+                  converted => converted.length <= 20,
+                  () => ({ message: importCsvRestrictionError })
+                ),
+                E.flatMap(converted => {
+                  const uniteId = GeoSystemes[params.geoSystemeId].uniteId
+                  let myPipe
+                  switch (uniteId) {
+                    case 'met': {
+                      myPipe = pipe(
+                        zodParseEither(csvXYValidator, converted),
+                        E.flatMap(rows =>
+                          E.tryCatch(
+                            () => {
+                              const coordinates: MultiPolygon['coordinates'] = [[rows.map(({ x, y }) => [x, y])]]
+                              const points: FeatureCollectionPoints['features'] = rows.map(ligne => ({
+                                type: 'Feature',
+                                properties: { nom: ligne.nom, description: ligne.description },
+                                geometry: { type: 'Point', coordinates: [ligne.x, ligne.y] },
+                              }))
+
+                              coordinates[0][0].push([rows[0].x, rows[0].y])
+
+                              return { coordinates, points }
+                            },
+                            e => ({ message: recuperationInfoCsvError, extra: e })
+                          )
+                        )
+                      )
+                      break
+                    }
+                    case 'gon':
+                    case 'deg': {
+                      myPipe = pipe(
+                        zodParseEither(csvLatLongValidator, converted),
+                        E.flatMap(rows =>
+                          E.tryCatch(
+                            () => {
+                              const coordinates: MultiPolygon['coordinates'] = [[rows.map(({ longitude, latitude }) => [longitude, latitude])]]
+                              const points: FeatureCollectionPoints['features'] = rows.map(ligne => ({
+                                type: 'Feature',
+                                properties: { nom: ligne.nom, description: ligne.description },
+                                geometry: { type: 'Point', coordinates: [ligne.longitude, ligne.latitude] },
+                              }))
+                              coordinates[0][0].push([rows[0].longitude, rows[0].latitude])
+
+                              return { coordinates, points }
+                            },
+                            e => ({ message: recuperationInfoCsvError, extra: e })
+                          )
+                        )
+                      )
+                      break
+                    }
+                    default: {
+                      exhaustiveCheck(uniteId)
+                      throw new Error('impossible')
+                    }
+                  }
+
+                  return pipe(
+                    myPipe,
+                    E.flatMap(({ coordinates, points }) => {
+                      const geojsonOriginFeatureMultiPolygon: FeatureMultiPolygon = { type: 'Feature', properties: {}, geometry: { type: 'MultiPolygon', coordinates } }
+                      const geojsonOriginFeatureCollectionPoints: FeatureCollectionPoints = { type: 'FeatureCollection', features: points }
+
+                      return E.right({ geojsonOriginFeatureMultiPolygon, geojsonOriginFeatureCollectionPoints })
+                    })
+                  )
+                })
+              )
             }
-
-            let coordinates: MultiPolygon['coordinates']
-            let points: FeatureCollectionPoints['features']
-            switch (uniteId) {
-              case 'met': {
-                const csvMetreToJsonValidator = makeCsvToJsonValidator(csvCommonLowerValidator.extend({ x: csvInputNumberValidator, y: csvInputNumberValidator }))
-                const rows = csvMetreToJsonValidator.parse(converted)
-                coordinates = [[rows.map(({ x, y }) => [x, y])]]
-                points = rows.map(ligne => ({
-                  type: 'Feature',
-                  properties: { nom: ligne.nom, description: ligne.description },
-                  geometry: { type: 'Point', coordinates: [ligne.x, ligne.y] },
-                }))
-
-                coordinates[0][0].push([rows[0].x, rows[0].y])
-                break
-              }
-              case 'gon':
-              case 'deg': {
-                const csvDegToJsonValidator = makeCsvToJsonValidator(csvCommonLowerValidator.extend({ longitude: csvInputNumberValidator, latitude: csvInputNumberValidator }))
-                const rows = csvDegToJsonValidator.parse(converted)
-                coordinates = [[rows.map(({ longitude, latitude }) => [longitude, latitude])]]
-                points = rows.map(ligne => ({
-                  type: 'Feature',
-                  properties: { nom: ligne.nom, description: ligne.description },
-                  geometry: { type: 'Point', coordinates: [ligne.longitude, ligne.latitude] },
-                }))
-                coordinates[0][0].push([rows[0].longitude, rows[0].latitude])
-                break
-              }
-              default:
-                exhaustiveCheck(uniteId)
-                throw new Error('Cas impossible mais typescript ne voit pas que exhaustiveCheck throw une exception')
-            }
-            geojsonOriginFeatureMultiPolygon = { type: 'Feature', properties: {}, geometry: { type: 'MultiPolygon', coordinates } }
-            geojsonOriginFeatureCollectionPoints = { type: 'FeatureCollection', features: points }
-            break
-          }
-          default: {
-            exhaustiveCheck(fileType)
-            throw new Error('Cas impossible mais typescript ne voit pas que exhaustiveCheck throw une exception')
+            default:
+              exhaustiveCheck(fileType)
+              throw new Error('Impossible')
           }
-        }
-        const geojson4326FeatureMultiPolygon = await getGeojsonByGeoSystemeIdQuery(pool, geoSystemeId.data, GEO_SYSTEME_IDS.WGS84, geojsonOriginFeatureMultiPolygon)
-        const geojson4326FeatureCollectionPoints =
-          geojsonOriginFeatureCollectionPoints !== null ? await convertPoints(pool, geoSystemeId.data, GEO_SYSTEME_IDS.WGS84, geojsonOriginFeatureCollectionPoints) : null
-
-        const geojson4326MultiPolygon: MultiPolygon = geojson4326FeatureMultiPolygon.geometry
-
-        z.array(polygon4326CoordinatesValidator).min(1).parse(geojson4326MultiPolygon.coordinates)
-
-        const geoInfo = await getGeojsonInformation(pool, geojson4326MultiPolygon)
-        const result: GeojsonInformations = {
-          superposition_alertes: await getAlertesSuperposition(geojson4326MultiPolygon, geojsonImportInput.data.titreTypeId, geojsonImportInput.data.titreSlug, user, pool),
-          communes: geoInfo.communes,
-          foretIds: geoInfo.forets,
-          sdomZoneIds: geoInfo.sdom,
-          secteurMaritimeIds: geoInfo.secteurs,
-          surface: geoInfo.surface,
-          geojson4326_perimetre: { type: 'Feature', geometry: geojson4326MultiPolygon, properties: {} },
-          geojson4326_points: geojson4326FeatureCollectionPoints,
-          geojson_origine_perimetre: geojsonOriginFeatureMultiPolygon,
-          geojson_origine_points: geojsonOriginFeatureCollectionPoints,
-          geojson_origine_geo_systeme_id: geoSystemeId.data,
-        }
-
-        res.json(result)
-      } catch (e: any) {
-        console.error(e)
-        res.status(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST).send(e)
+        })
+      )
+    ),
+    TE.bindW('geojson4326MultiPolygon', ({ geojsonOriginFeatureMultiPolygon }) =>
+      pipe(
+        getGeojsonByGeoSystemeIdQuery(pool, params.geoSystemeId, GEO_SYSTEME_IDS.WGS84, geojsonOriginFeatureMultiPolygon),
+        TE.map(result => result.geometry)
+      )
+    ),
+    TE.bindW('geojson4326FeatureCollectionPoints', ({ geojsonOriginFeatureCollectionPoints }) => {
+      if (isNotNullNorUndefined(geojsonOriginFeatureCollectionPoints)) {
+        return convertPoints(pool, params.geoSystemeId, GEO_SYSTEME_IDS.WGS84, geojsonOriginFeatureCollectionPoints)
       }
-    } else {
-      res.status(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST).send(geojsonImportInput.error)
-    }
-  }
-}
 
-export const geojsonImportPoints = (pool: Pool) => async (req: CaminoRequest, res: CustomResponse<GeojsonImportPointsResponse>) => {
-  const user = req.auth
-
-  const geoSystemeId = geoSystemeIdValidator.safeParse(req.params.geoSystemeId)
-  if (!user || isDefault(user)) {
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else if (!geoSystemeId.success) {
-    console.warn(`le geoSystemeId est obligatoire`)
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else {
-    const geojsonImportInput = geojsonImportPointBodyValidator.safeParse(req.body)
-
-    if (geojsonImportInput.success) {
-      try {
-        const filename = geojsonImportInput.data.tempDocumentName
+      return TE.right(null)
+    }),
+    TE.bindW('geoInfo', ({ geojson4326MultiPolygon }) => getGeojsonInformation(pool, geojson4326MultiPolygon)),
+    TE.bindW('alertesSuperposition', ({ geojson4326MultiPolygon }) => getAlertesSuperposition(geojson4326MultiPolygon, body.titreTypeId, body.titreSlug, user, pool)),
+    TE.map(({ geojson4326MultiPolygon, geojson4326FeatureCollectionPoints, geoInfo, alertesSuperposition, geojsonOriginFeatureMultiPolygon, geojsonOriginFeatureCollectionPoints }) => {
+      const result: GeojsonInformations = {
+        superposition_alertes: alertesSuperposition,
+        communes: geoInfo.communes,
+        foretIds: geoInfo.forets,
+        sdomZoneIds: geoInfo.sdom,
+        secteurMaritimeIds: geoInfo.secteurs,
+        surface: geoInfo.surface,
+        geojson4326_perimetre: { type: 'Feature', geometry: geojson4326MultiPolygon, properties: {} },
+        geojson4326_points: geojson4326FeatureCollectionPoints,
+        geojson_origine_perimetre: geojsonOriginFeatureMultiPolygon,
+        geojson_origine_points: geojsonOriginFeatureCollectionPoints,
+        geojson_origine_geo_systeme_id: params.geoSystemeId,
+      }
 
-        const pathFrom = join(process.cwd(), `/files/tmp/${filename}`)
-        const fileContent = readFileSync(pathFrom)
-        const features = featureCollectionPointsValidator.parse(JSON.parse(fileContent.toString()))
-        const conversion = await convertPoints(pool, geoSystemeId.data, GEO_SYSTEME_IDS.WGS84, features)
-        res.json({ geojson4326: conversion, origin: features })
-      } catch (e: any) {
-        console.error(e)
-        res.status(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST).send(e)
+      return result
+    }),
+    TE.mapLeft(caminoError => {
+      const message = caminoError.message
+      switch (message) {
+        case 'Une erreur est survenue lors de la lecture du csv':
+        case 'Une erreur est survenue lors de la récupération des informations du CSV':
+        case 'Une erreur inattendue est survenue lors de la récupération des informations geojson en base':
+        case "Impossible d'accéder à la base de données":
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR }
+        case 'Problème de validation de données':
+        case "Une erreur s'est produite lors de l'ouverture du fichier GeoJSON":
+        case "Une erreur s'est produite lors de l'ouverture du fichier shape":
+        case 'Impossible de convertir la géométrie en JSON':
+        case 'Impossible de convertir le geojson vers le système':
+        case "L'import CSV est fait pour des petits polygones simple de moins de 20 sommets":
+        case "Le périmètre n'est pas valide dans le référentiel donné":
+        case "Une erreur s'est produite lors de l'extraction du multi-polygone du fichier GeoJSON":
+        case "Une erreur s'est produite lors de l'extraction du multi-polygone du fichier shape":
+        case 'Impossible de transformer la feature collection':
+        case 'La liste des points est vide':
+        case 'Le nombre de points est invalide':
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_BAD_REQUEST }
+        default:
+          exhaustiveCheck(message)
+          throw new Error('impossible')
       }
-    } else {
-      res.status(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST).send(geojsonImportInput.error)
-    }
-  }
+    })
+  )
 }
+const fileNameToJson = <T extends ZodTypeAny>(pathFrom: string, validator: T): E.Either<CaminoError<ZodUnparseable | typeof ouvertureGeoJSONError>, z.infer<T>> => {
+  return pipe(
+    E.tryCatch(
+      () => {
+        const fileContent = readFileSync(pathFrom)
 
-export const geojsonImportForages = (pool: Pool) => async (req: CaminoRequest, res: CustomResponse<GeojsonImportForagesResponse>) => {
-  const user = req.auth
-
-  const geoSystemeId = geoSystemeIdValidator.safeParse(req.params.geoSystemeId)
-  if (!user || isDefault(user)) {
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else if (!geoSystemeId.success) {
-    console.warn(`le geoSystemeId est obligatoire`)
-    res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
-  } else {
-    const geojsonImportInput = geojsonImportForagesBodyValidator.safeParse(req.body)
-
-    if (geojsonImportInput.success) {
-      try {
-        const filename = geojsonImportInput.data.tempDocumentName
-
-        const pathFrom = join(process.cwd(), `/files/tmp/${filename}`)
-        const fileType = geojsonImportInput.data.fileType
-        let features: FeatureCollectionForages
-        switch (fileType) {
-          case 'geojson': {
-            const fileContent = readFileSync(pathFrom)
-            features = featureCollectionForagesValidator.parse(JSON.parse(fileContent.toString()))
-
-            break
-          }
-
-          case 'shp': {
-            const fileContent = readFileSync(pathFrom)
-            const shpParsed = shpjs.parseShp(fileContent)
-            features = featureCollectionForagesValidator.parse(shpParsed)
-
-            break
-          }
-
-          case 'csv': {
-            const fileContent = readFileSync(pathFrom, { encoding: 'utf-8' })
-            const result = xlsx.read(fileContent, { type: 'string', FS: ';', raw: true })
+        return JSON.parse(fileContent.toString())
+      },
+      () => ({ message: ouvertureGeoJSONError })
+    ),
+    E.flatMap(zodParseEitherCallback(validator))
+  )
+}
+const fileNameToShape = <T extends ZodTypeAny>(pathFrom: string, validator: T): E.Either<CaminoError<ZodUnparseable | typeof ouvertureShapeError>, z.infer<T>> => {
+  return pipe(
+    E.tryCatch(
+      () => {
+        const fileContent = readFileSync(pathFrom)
 
-            if (result.SheetNames.length !== 1) {
-              throw new Error(`une erreur est survenue lors de la lecture du csv, il ne devrait y avoir qu'un seul document ${result.SheetNames}`)
-            }
-            const sheet1 = result.Sheets[result.SheetNames[0]]
-            const converted = xlsx.utils.sheet_to_json(sheet1, { raw: true })
-            const uniteId = GeoSystemes[geoSystemeId.data].uniteId
+        return shpjs.parseShp(fileContent)
+      },
+      () => ({ message: ouvertureShapeError })
+    ),
+    E.flatMap(zodParseEitherCallback(validator))
+  )
+}
+const fileNameToCsv = (pathFrom: string): E.Either<CaminoError<typeof ouvertureCsvError>, unknown[]> => {
+  return E.tryCatch(
+    () => {
+      const fileContent = readFileSync(pathFrom, { encoding: 'utf-8' })
+      const result = xlsx.read(fileContent, { type: 'string', FS: ';', raw: true })
+
+      if (result.SheetNames.length !== 1) {
+        throw new Error(`une erreur est survenue lors de la lecture du csv, il ne devrait y avoir qu'un seul document ${result.SheetNames}`)
+      }
 
-            let coordinates: MultiPolygon['coordinates']
-            let points: FeatureCollectionForages['features']
+      const sheet1 = result.Sheets[result.SheetNames[0]]
 
-            switch (uniteId) {
-              case 'met': {
-                const csvMetreToJsonValidator = makeCsvToJsonValidator(
-                  featureForagePropertiesValidator.omit({ profondeur: true }).extend({ x: csvInputNumberValidator, y: csvInputNumberValidator, profondeur: csvInputNumberValidator })
-                )
+      return xlsx.utils.sheet_to_json(sheet1, { raw: true })
+    },
+    e => ({ message: ouvertureCsvError, extra: e })
+  )
+}
 
-                const rows = csvMetreToJsonValidator.parse(converted)
-                coordinates = [[rows.map(({ x, y }) => [x, y])]]
-                points = rows.map(ligne => ({
-                  type: 'Feature',
-                  properties: { nom: ligne.nom, description: ligne.description, profondeur: ligne.profondeur, type: ligne.type },
-                  geometry: { type: 'Point', coordinates: [ligne.x, ligne.y] },
-                }))
+type GeosjsonImportPointsErrorMessages = ZodUnparseable | DbQueryAccessError | 'Accès interdit' | 'Fichier incorrect' | ConvertPointsErrors
+export const geojsonImportPoints = (
+  pool: Pool,
+  user: DeepReadonly<UserNotNull>,
+  geojsonImportInput: DeepReadonly<GeojsonImportPointsBody>,
+  params: { geoSystemeId: GeoSystemeId }
+): TE.TaskEither<CaminoApiError<GeosjsonImportPointsErrorMessages>, GeojsonImportPointsResponse> => {
+  return pipe(
+    TE.Do,
+    TE.filterOrElseW(
+      () => !isDefault(user),
+      () => ({ message: 'Accès interdit' as const })
+    ),
+    TE.bindW('features', () => {
+      return TE.fromEither(
+        pipe(
+          E.tryCatch(
+            () => {
+              const filename = geojsonImportInput.tempDocumentName
+              const pathFrom = join(process.cwd(), `/files/tmp/${filename}`)
+              const fileContent = readFileSync(pathFrom)
+
+              return JSON.parse(fileContent.toString())
+            },
+            () => ({ message: 'Fichier incorrect' as const })
+          ),
+          E.flatMap(zodParseEitherCallback(featureCollectionPointsValidator))
+        )
+      )
+    }),
+    TE.bindW('geojson4326points', ({ features }) => convertPoints(pool, params.geoSystemeId, GEO_SYSTEME_IDS.WGS84, features)),
+    TE.map(result => ({ geojson4326: result.geojson4326points, origin: result.features })),
+    TE.mapLeft(caminoError => {
+      const message = caminoError.message
+      switch (message) {
+        case 'Accès interdit':
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_FORBIDDEN }
+        case "Impossible d'accéder à la base de données":
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR }
+        case 'Problème de validation de données':
+        case 'Fichier incorrect':
+        case 'Impossible de transformer la feature collection':
+        case 'La liste des points est vide':
+        case 'Le nombre de points est invalide':
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_BAD_REQUEST }
+        default:
+          exhaustiveCheck(message)
+          throw new Error('impossible')
+      }
+    })
+  )
+}
 
-                coordinates[0][0].push([rows[0].x, rows[0].y])
-                break
-              }
-              case 'gon':
-              case 'deg': {
-                const csvDegToJsonValidator = makeCsvToJsonValidator(
-                  featureForagePropertiesValidator.omit({ profondeur: true }).extend({ longitude: csvInputNumberValidator, latitude: csvInputNumberValidator, profondeur: csvInputNumberValidator })
-                )
-                const rows = csvDegToJsonValidator.parse(converted)
-                coordinates = [[rows.map(({ longitude, latitude }) => [longitude, latitude])]]
-                points = rows.map(ligne => ({
-                  type: 'Feature',
-                  properties: { nom: ligne.nom, description: ligne.description, profondeur: ligne.profondeur, type: ligne.type },
-                  geometry: { type: 'Point', coordinates: [ligne.longitude, ligne.latitude] },
-                }))
-                coordinates[0][0].push([rows[0].longitude, rows[0].latitude])
-                break
+type GeosjsonImportForagesErrorMessages = ZodUnparseable | DbQueryAccessError | typeof ouvertureCsvError | typeof ouvertureShapeError | typeof ouvertureGeoJSONError | ConvertPointsErrors
+export const geojsonImportForages = (
+  pool: Pool,
+  _user: DeepReadonly<UserNotNull>,
+  body: DeepReadonly<GeojsonImportForagesBody>,
+  params: { geoSystemeId: GeoSystemeId }
+): TE.TaskEither<CaminoApiError<GeosjsonImportForagesErrorMessages>, GeojsonImportForagesResponse> => {
+  const filename = body.tempDocumentName
+
+  const pathFrom = join(process.cwd(), `/files/tmp/${filename}`)
+  const fileType = body.fileType
+
+  return pipe(
+    TE.Do,
+    TE.bindW<'features', object, CaminoError<GeosjsonImportForagesErrorMessages>, FeatureCollectionForages>('features', () => {
+      let myPipe: E.Either<CaminoError<GeosjsonImportForagesErrorMessages>, z.infer<typeof featureCollectionForagesValidator>>
+      switch (fileType) {
+        case 'geojson': {
+          myPipe = fileNameToJson(pathFrom, featureCollectionForagesValidator)
+          break
+        }
+        case 'shp': {
+          myPipe = fileNameToShape(pathFrom, featureCollectionForagesValidator)
+          break
+        }
+        case 'csv': {
+          myPipe = pipe(
+            fileNameToCsv(pathFrom),
+            E.flatMap(converted => {
+              const uniteId = GeoSystemes[params.geoSystemeId].uniteId
+              switch (uniteId) {
+                case 'met': {
+                  return pipe(
+                    zodParseEither(csvForageXYValidator, converted),
+                    E.map(rows => {
+                      const points: FeatureCollectionForages['features'] = rows.map(ligne => ({
+                        type: 'Feature',
+                        properties: { nom: ligne.nom, description: ligne.description, profondeur: ligne.profondeur, type: ligne.type },
+                        geometry: { type: 'Point', coordinates: [ligne.x, ligne.y] },
+                      }))
+
+                      return { type: 'FeatureCollection', features: points } as const
+                    })
+                  )
+                }
+                case 'gon':
+                case 'deg': {
+                  return pipe(
+                    zodParseEither(csvForageDegValidator, converted),
+                    E.map(rows => {
+                      const points: FeatureCollectionForages['features'] = rows.map(ligne => ({
+                        type: 'Feature',
+                        properties: { nom: ligne.nom, description: ligne.description, profondeur: ligne.profondeur, type: ligne.type },
+                        geometry: { type: 'Point', coordinates: [ligne.longitude, ligne.latitude] },
+                      }))
+
+                      return { type: 'FeatureCollection', features: points } as const
+                    })
+                  )
+                }
+                default:
+                  exhaustiveCheck(uniteId)
+                  throw new Error('Cas impossible mais typescript ne voit pas que exhaustiveCheck throw une exception')
               }
-              default:
-                exhaustiveCheck(uniteId)
-                throw new Error('Cas impossible mais typescript ne voit pas que exhaustiveCheck throw une exception')
-            }
-            features = { type: 'FeatureCollection', features: points }
-            break
-          }
-          default: {
-            exhaustiveCheck(fileType)
-            throw new Error('Cas impossible mais typescript ne voit pas que exhaustiveCheck throw une exception')
-          }
+            }),
+            E.flatMap(zodParseEitherCallback(featureCollectionForagesValidator))
+          )
+          break
         }
 
-        const conversion = await convertPoints(pool, geoSystemeId.data, GEO_SYSTEME_IDS.WGS84, features)
-        res.json({ geojson4326: conversion, origin: features })
-      } catch (e: any) {
-        console.error(e)
-        res.status(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST).send(e)
+        default: {
+          exhaustiveCheck(fileType)
+          throw new Error('Cas impossible mais typescript ne voit pas que exhaustiveCheck throw une exception')
+        }
       }
-    } else {
-      res.status(HTTP_STATUS.HTTP_STATUS_BAD_REQUEST).send(geojsonImportInput.error)
-    }
-  }
+
+      return TE.fromEither(myPipe)
+    }),
+    TE.bindW('conversion', ({ features }) => convertPoints(pool, params.geoSystemeId, GEO_SYSTEME_IDS.WGS84, features)),
+    TE.map(({ conversion, features }) => {
+      return { geojson4326: conversion, origin: features }
+    }),
+    TE.mapLeft(caminoError => {
+      const message = caminoError.message
+      switch (message) {
+        case "Impossible d'accéder à la base de données":
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR }
+        case 'Problème de validation de données':
+        case 'Une erreur est survenue lors de la lecture du csv':
+        case "Une erreur s'est produite lors de l'ouverture du fichier GeoJSON":
+        case "Une erreur s'est produite lors de l'ouverture du fichier shape":
+        case 'Impossible de transformer la feature collection':
+        case 'La liste des points est vide':
+        case 'Le nombre de points est invalide':
+          return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_BAD_REQUEST }
+        default:
+          exhaustiveCheck(message)
+          throw new Error('impossible')
+      }
+    })
+  )
 }
 
-const getAlertesSuperposition = async (geojson4326_perimetre: MultiPolygon | null, titreTypeId: TitreTypeId, titreSlug: TitreSlug, user: User, pool: Pool) => {
+const getAlertesSuperposition = (
+  geojson4326_perimetre: MultiPolygon | null,
+  titreTypeId: TitreTypeId,
+  titreSlug: TitreSlug,
+  user: DeepReadonly<User>,
+  pool: Pool
+): TE.TaskEither<CaminoError<ZodUnparseable | DbQueryAccessError>, GetTitresIntersectionWithGeojson[]> => {
   if (titreTypeId === 'axm' && (isSuper(user) || isAdministrationAdmin(user) || isAdministrationEditeur(user)) && geojson4326_perimetre !== null) {
     // vérifie qu’il n’existe pas de demandes de titres en cours sur ce périmètre
     return getTitresIntersectionWithGeojson(pool, geojson4326_perimetre, titreSlug)
   }
 
-  return []
+  return TE.right([])
 }
diff --git a/packages/api/src/api/rest/utilisateurs.test.integration.ts b/packages/api/src/api/rest/utilisateurs.test.integration.ts
index 1ddb07d8409744be02dde841ce39bd79c947d7f3..5cd471b9ad2baf335000d64a3bbb23b5f8162971 100644
--- a/packages/api/src/api/rest/utilisateurs.test.integration.ts
+++ b/packages/api/src/api/rest/utilisateurs.test.integration.ts
@@ -9,6 +9,7 @@ import { userSuper } from '../../database/user-super.js'
 import { newUtilisateurId } from '../../database/models/_format/id-create.js'
 import { KeycloakFakeServer, idUserKeycloakRecognised, setupKeycloak, teardownKeycloak } from '../../../tests/keycloak.js'
 import { renewConfig } from '../../config/index.js'
+import { utilisateurIdValidator } from 'camino-common/src/roles.js'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -87,7 +88,7 @@ describe('utilisateurModifier', () => {
 
 describe('utilisateurSupprimer', () => {
   test('ne peut pas supprimer un compte (utilisateur anonyme)', async () => {
-    const tested = await restCall(dbPool, '/rest/utilisateurs/:id/delete', { id: 'test' }, undefined)
+    const tested = await restCall(dbPool, '/rest/utilisateurs/:id/delete', { id: utilisateurIdValidator.parse('test') }, undefined)
     expect(tested.statusCode).toBe(500)
     expect(tested.body).toMatchInlineSnapshot(`
       {
@@ -108,7 +109,7 @@ describe('utilisateurSupprimer', () => {
   })
 
   test('peut supprimer un utilisateur (utilisateur super)', async () => {
-    const id = 'user-todelete'
+    const id = utilisateurIdValidator.parse('user-todelete')
     await knex('utilisateurs').insert({
       id,
       prenom: 'userToDelete',
@@ -124,7 +125,7 @@ describe('utilisateurSupprimer', () => {
   })
 
   test('ne peut pas supprimer un utilisateur inexistant (utilisateur super)', async () => {
-    const tested = await restCall(dbPool, '/rest/utilisateurs/:id/delete', { id: 'not-existing' }, { role: 'super' })
+    const tested = await restCall(dbPool, '/rest/utilisateurs/:id/delete', { id: utilisateurIdValidator.parse('not-existing') }, { role: 'super' })
     expect(tested.statusCode).toBe(500)
     expect(tested.body).toMatchInlineSnapshot(`
       {
diff --git a/packages/api/src/business/processes/titres-etapes-areas-update.ts b/packages/api/src/business/processes/titres-etapes-areas-update.ts
index 2517d07603321d9f74d3986847add3bcc3f63271..65bca2336ecae2e6a8144bb9eba0b665296438b4 100644
--- a/packages/api/src/business/processes/titres-etapes-areas-update.ts
+++ b/packages/api/src/business/processes/titres-etapes-areas-update.ts
@@ -6,11 +6,12 @@ import { SecteursMaritimes, SecteursMaritimesIds, getSecteurMaritime } from 'cam
 import { knex } from '../../knex.js'
 import { ForetId } from 'camino-common/src/static/forets.js'
 import { CommuneId, toCommuneId } from 'camino-common/src/static/communes.js'
-import { isNotNullNorUndefined, onlyUnique } from 'camino-common/src/typescript-tools.js'
+import { DeepReadonly, isNotNullNorUndefined, onlyUnique } from 'camino-common/src/typescript-tools.js'
 import { getGeojsonInformation } from '../../api/rest/perimetre.queries.js'
 import type { Pool } from 'pg'
 import { SDOMZoneId } from 'camino-common/src/static/sdom.js'
 import { M2 } from 'camino-common/src/number.js'
+import { isRight } from 'fp-ts/lib/Either.js'
 
 /**
  * Met à jour tous les territoires d’une liste d’étapes
@@ -40,21 +41,26 @@ export const titresEtapesAreasUpdate = async (pool: Pool, titresEtapesIds?: stri
     }
     try {
       if (isNotNullNorUndefined(titreEtape.geojson4326Perimetre)) {
-        const { forets, sdom, secteurs, communes } = await getGeojsonInformation(pool, titreEtape.geojson4326Perimetre.geometry)
+        const result = await getGeojsonInformation(pool, titreEtape.geojson4326Perimetre.geometry)()
+        if (isRight(result)) {
+          const { forets, sdom, secteurs, communes } = result.right
 
-        await intersectForets(titreEtape, forets)
-        await intersectSdom(titreEtape, sdom)
-        await intersectCommunes(titreEtape, communes)
-        await intersectSecteursMaritime(titreEtape, secteurs)
+          await intersectForets(titreEtape, forets)
+          await intersectSdom(titreEtape, sdom)
+          await intersectCommunes(titreEtape, communes)
+          await intersectSecteursMaritime(titreEtape, secteurs)
+        } else {
+          throw new Error(result.left.message)
+        }
       }
     } catch (e) {
       console.error(`Une erreur est survenue lors du traitement de l'étape ${titreEtape.id}`)
     }
   }
 }
-async function intersectSdom(titreEtape: Pick<ITitreEtape, 'sdomZones' | 'id'>, sdomZonesIds: SDOMZoneId[]) {
+async function intersectSdom(titreEtape: Pick<ITitreEtape, 'sdomZones' | 'id'>, sdomZonesIds: DeepReadonly<SDOMZoneId[]>) {
   if (sdomZonesIds.length > 0) {
-    const sortedSdomZonesIds = [...sdomZonesIds.sort()]
+    const sortedSdomZonesIds = sdomZonesIds.toSorted()
 
     if (sortedSdomZonesIds.length !== titreEtape.sdomZones?.length || titreEtape.sdomZones?.some((elem, index) => elem !== sortedSdomZonesIds[index])) {
       console.info(`nouvelles zones du sdom pour l'étape ${titreEtape.id}. Anciennes: ${JSON.stringify(titreEtape.sdomZones)}, nouvelles: ${JSON.stringify(sortedSdomZonesIds)}`)
@@ -63,12 +69,12 @@ async function intersectSdom(titreEtape: Pick<ITitreEtape, 'sdomZones' | 'id'>,
   }
 }
 
-async function intersectForets(titreEtape: Pick<ITitreEtape, 'forets' | 'id'>, foretsNew: ForetId[]) {
+async function intersectForets(titreEtape: Pick<ITitreEtape, 'forets' | 'id'>, foretsNew: DeepReadonly<ForetId[]>) {
   if (!titreEtape.forets) {
     throw new Error('les forêts de l’étape ne sont pas chargées')
   }
 
-  const sortedForets = [...foretsNew.sort()]
+  const sortedForets = foretsNew.toSorted()
 
   if (titreEtape.forets?.length !== sortedForets.length || titreEtape.forets.some((value, index) => value !== sortedForets[index])) {
     console.info(`Mise à jour des forêts sur l'étape ${titreEtape.id}, ancien: '${JSON.stringify(titreEtape.forets)}', nouveaux: '${JSON.stringify(sortedForets)}'`)
@@ -78,7 +84,7 @@ async function intersectForets(titreEtape: Pick<ITitreEtape, 'forets' | 'id'>, f
   }
 }
 
-async function intersectCommunes(titreEtape: Pick<ITitreEtape, 'communes' | 'id' | 'geojson4326Perimetre'>, communes: { id: CommuneId; surface: M2 }[]) {
+async function intersectCommunes(titreEtape: Pick<ITitreEtape, 'communes' | 'id' | 'geojson4326Perimetre'>, communes: DeepReadonly<{ id: CommuneId; surface: M2 }[]>) {
   const communesNew: { id: CommuneId; surface: M2 }[] = communes.map(({ id, surface }) => ({ id: toCommuneId(id), surface })).sort((a, b) => a.id.localeCompare(b.id))
   if (titreEtape.communes?.length !== communesNew.length || titreEtape.communes.some((value, index) => value.id !== communesNew[index].id || value.surface !== communesNew[index].surface)) {
     console.info(`Mise à jour des communes sur l'étape ${titreEtape.id}, ancien: '${JSON.stringify(titreEtape.communes)}', nouveaux: '${JSON.stringify(communesNew)}'`)
@@ -88,7 +94,7 @@ async function intersectCommunes(titreEtape: Pick<ITitreEtape, 'communes' | 'id'
   }
 }
 
-async function intersectSecteursMaritime(titreEtape: Pick<ITitreEtape, 'secteursMaritime' | 'id'>, secteursMaritime: SecteursMaritimesIds[]) {
+async function intersectSecteursMaritime(titreEtape: Pick<ITitreEtape, 'secteursMaritime' | 'id'>, secteursMaritime: DeepReadonly<SecteursMaritimesIds[]>) {
   const secteurMaritimeNew: SecteursMaritimes[] = [...secteursMaritime.map(id => getSecteurMaritime(id))].filter(onlyUnique).sort()
   if (titreEtape.secteursMaritime?.length !== secteurMaritimeNew.length || titreEtape.secteursMaritime.some((value, index) => value !== secteurMaritimeNew[index])) {
     console.info(`Mise à jour des secteurs maritimes sur l'étape ${titreEtape.id}, ancien: '${titreEtape.secteursMaritime}', nouveaux: '${secteurMaritimeNew}'`)
diff --git a/packages/api/src/business/validations/titre-etape-updation-validate.ts b/packages/api/src/business/validations/titre-etape-updation-validate.ts
index 08211ae0c83957f6be58c0ec3675ee2cbd3c8f51..ee5d5937a75e1499c2dc267cec73fb183fbe9de8 100644
--- a/packages/api/src/business/validations/titre-etape-updation-validate.ts
+++ b/packages/api/src/business/validations/titre-etape-updation-validate.ts
@@ -10,7 +10,7 @@ import { getSections } from 'camino-common/src/static/titresTypes_demarchesTypes
 import { EntrepriseDocument, EntrepriseId } from 'camino-common/src/entreprise.js'
 import { ETAPE_IS_NOT_BROUILLON, EtapeAvis, EtapeDocument, GetEtapeDocumentsByEtapeIdAslDocument, GetEtapeDocumentsByEtapeIdDaeDocument } from 'camino-common/src/etape.js'
 import { CommuneId } from 'camino-common/src/static/communes.js'
-import { isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools.js'
+import { DeepReadonly, isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools.js'
 import { FlattenEtape } from 'camino-common/src/etape-form.js'
 import { flattenContenuToSimpleContenu } from 'camino-common/src/sections.js'
 
@@ -21,7 +21,7 @@ export const titreEtapeUpdationValidate = (
   documents: Pick<EtapeDocument, 'etape_document_type_id'>[],
   etapeAvis: Pick<EtapeAvis, 'avis_type_id'>[],
   entrepriseDocuments: Pick<EntrepriseDocument, 'entreprise_document_type_id' | 'entreprise_id'>[],
-  sdomZones: SDOMZoneId[] | null | undefined,
+  sdomZones: DeepReadonly<SDOMZoneId[]> | null | undefined,
   communes: CommuneId[] | null | undefined,
   user: User,
   daeDocument: Omit<GetEtapeDocumentsByEtapeIdDaeDocument, 'id'> | null,
diff --git a/packages/api/src/pg-database.ts b/packages/api/src/pg-database.ts
index 9c2f7fcbd3f3495b04048009bc1366644e6d59c6..0ddc6c7cef54e93bbc4353556f6fb677dadc314d 100644
--- a/packages/api/src/pg-database.ts
+++ b/packages/api/src/pg-database.ts
@@ -1,7 +1,13 @@
 import { TaggedQuery } from '@pgtyped/runtime'
+
+import TE from 'fp-ts/lib/TaskEither.js'
+
 import type { Pool } from 'pg'
 import { z } from 'zod'
 import type { ZodType, ZodTypeDef } from 'zod'
+import { pipe } from 'fp-ts/lib/function.js'
+import { CaminoError } from 'camino-common/src/zod-tools.js'
+import { ZodUnparseable, zodParseTaskEitherCallback } from './tools/fp-tools.js'
 export type Redefine<T, P, O> = T extends { params: infer A; result: infer B }
   ? { inputs: keyof A; outputs: keyof B } extends { inputs: keyof P; outputs: keyof O }
     ? { inputs: keyof P; outputs: keyof O } extends { inputs: keyof A; outputs: keyof B }
@@ -10,6 +16,9 @@ export type Redefine<T, P, O> = T extends { params: infer A; result: infer B }
     : { __camino_error: 'toutes les clés de pgtyped ne sont pas présentes dans redefine' }
   : { __camino_error: 'on a pas params et result' }
 
+/**
+ * @deprecated use newDbQueryAndValidate
+ */
 export const dbQueryAndValidate = async <Params, Result, T extends ZodType<Result, ZodTypeDef, unknown>>(
   query: TaggedQuery<{ params: Params; result: Result }>,
   params: Params,
@@ -21,3 +30,29 @@ export const dbQueryAndValidate = async <Params, Result, T extends ZodType<Resul
 
   return z.array(validator).parse(result)
 }
+
+export type DbQueryAccessError = "Impossible d'accéder à la base de données"
+export const newDbQueryAndValidate = <Params, Result, T extends ZodType<Result, ZodTypeDef, unknown>>(
+  query: TaggedQuery<{ params: Params; result: Result }>,
+  params: Params,
+  pool: Pool,
+  validator: T
+): TE.TaskEither<CaminoError<DbQueryAccessError | ZodUnparseable>, Result[]> => {
+  return pipe(
+    TE.tryCatch(
+      // eslint-disable-next-line no-restricted-syntax
+      () => query.run(params, pool),
+      e => {
+        let extra = ''
+        if (typeof e === 'string') {
+          extra = e.toUpperCase()
+        } else if (e instanceof Error) {
+          extra = e.message
+        }
+
+        return { message: "Impossible d'accéder à la base de données" as const, extra }
+      }
+    ),
+    TE.flatMap(zodParseTaskEitherCallback(z.array(validator)))
+  )
+}
diff --git a/packages/api/src/server/rest.ts b/packages/api/src/server/rest.ts
index cf46627d8a6ea6a2cad095b5e7de424a3468fc6f..b92f032d329ec0005b5c7aa40e940a9278bf6487 100644
--- a/packages/api/src/server/rest.ts
+++ b/packages/api/src/server/rest.ts
@@ -1,8 +1,9 @@
 /* eslint-disable @typescript-eslint/ban-types */
 
-import { Index } from '../types.js'
+import { CaminoApiError, Index } from '../types.js'
 import type { Pool } from 'pg'
 
+import TE from 'fp-ts/lib/TaskEither.js'
 import express from 'express'
 import { join } from 'path'
 import { inspect } from 'node:util'
@@ -36,10 +37,11 @@ import {
   DownloadRestRoutes,
   CaminoRestRoute,
   NewDownloadRestRoutes,
+  NewPostRestRoutes,
 } from 'camino-common/src/rest.js'
 import { CaminoConfig, caminoConfigValidator } from 'camino-common/src/static/config.js'
 import { CaminoRequest, CustomResponse } from '../api/rest/express-type.js'
-import { User } from 'camino-common/src/roles.js'
+import { User, UserNotNull } from 'camino-common/src/roles.js'
 import {
   createEtape,
   deleteEtape,
@@ -57,13 +59,15 @@ import { SendFileOptions } from 'express-serve-static-core'
 import { activiteDocumentDownload, getActivite, updateActivite, deleteActivite } from '../api/rest/activites.js'
 import { DeepReadonly, isNotNullNorUndefined } from 'camino-common/src/typescript-tools.js'
 import { getDemarcheByIdOrSlug } from '../api/rest/demarches.js'
-import { geojsonImport, geojsonImportPoints, convertGeojsonPointsToGeoSystemeId, getPerimetreInfos, geojsonImportForages } from '../api/rest/perimetre.js'
+import { geojsonImport, geojsonImportPoints, getPerimetreInfos, geojsonImportForages } from '../api/rest/perimetre.js'
 import { getDataGouvStats } from '../api/rest/statistiques/datagouv.js'
 import { addAdministrationActiviteTypeEmails, deleteAdministrationActiviteTypeEmails, getAdministrationActiviteTypeEmails, getAdministrationUtilisateurs } from '../api/rest/administrations.js'
 import { titreDemandeCreer } from '../api/rest/titre-demande.js'
 import { config } from '../config/index.js'
 import { addLog } from '../api/rest/logs.queries.js'
 import { HTTP_STATUS } from 'camino-common/src/http.js'
+import { pipe } from 'fp-ts/lib/function.js'
+import { zodParseTaskEither } from '../tools/fp-tools.js'
 
 interface IRestResolverResult {
   nom: string
@@ -84,18 +88,26 @@ type IRestResolver = (
   user: User
 ) => Promise<IRestResolverResult | null>
 
-type RestGetCall<Route extends GetRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<DeepReadonly<z.infer<(typeof CaminoRestRoutes)[Route]['get']['output']>>>) => Promise<void>
-type RestPostCall<Route extends PostRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<(typeof CaminoRestRoutes)[Route]['post']['output']>>) => Promise<void>
-type RestPutCall<Route extends PutRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<(typeof CaminoRestRoutes)[Route]['put']['output']>>) => Promise<void>
+type CaminoRestRoutesType = typeof CaminoRestRoutes
+type RestGetCall<Route extends GetRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<DeepReadonly<z.infer<CaminoRestRoutesType[Route]['get']['output']>>>) => Promise<void>
+type RestNewPostCall<Route extends NewPostRestRoutes> = (
+  pool: Pool,
+  user: DeepReadonly<UserNotNull>,
+  body: DeepReadonly<z.infer<CaminoRestRoutesType[Route]['newPost']['input']>>,
+  params: DeepReadonly<z.infer<CaminoRestRoutesType[Route]['params']>>
+) => TE.TaskEither<CaminoApiError<string>, DeepReadonly<z.infer<CaminoRestRoutesType[Route]['newPost']['output']>>>
+type RestPostCall<Route extends PostRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<CaminoRestRoutesType[Route]['post']['output']>>) => Promise<void>
+type RestPutCall<Route extends PutRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<CaminoRestRoutesType[Route]['put']['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 ? { get: RestGetCall<Route> } : {}) &
-  (Route extends PostRestRoutes ? { post: RestPostCall<Route> } : {}) &
-  (Route extends PutRestRoutes ? { put: RestPutCall<Route> } : {}) &
-  (Route extends DeleteRestRoutes ? { delete: RestDeleteCall } : {}) &
-  (Route extends NewDownloadRestRoutes ? { newDownload: NewDownload } : {}) &
-  (Route extends DownloadRestRoutes ? { download: RestDownloadCall } : {})
+type Transform<Route> = (Route extends GetRestRoutes ? { getCall: RestGetCall<Route> } : {}) &
+  (Route extends PostRestRoutes ? { postCall: RestPostCall<Route> } : {}) &
+  (Route extends NewPostRestRoutes ? { newPostCall: RestNewPostCall<Route> } : {}) &
+  (Route extends PutRestRoutes ? { putCall: RestPutCall<Route> } : {}) &
+  (Route extends DeleteRestRoutes ? { deleteCall: RestDeleteCall } : {}) &
+  (Route extends NewDownloadRestRoutes ? { newDownloadCall: NewDownload } : {}) &
+  (Route extends DownloadRestRoutes ? { downloadCall: RestDownloadCall } : {})
 
 const getConfig = (_pool: Pool) => async (_req: CaminoRequest, res: CustomResponse<CaminoConfig>) => {
   const caminoConfig: CaminoConfig = {
@@ -107,71 +119,76 @@ const getConfig = (_pool: Pool) => async (_req: CaminoRequest, res: CustomRespon
   res.json(caminoConfigValidator.parse(caminoConfig))
 }
 
-const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<key> }> = {
+const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<key> & CaminoRestRoutesType[key] }> = {
   // NE PAS TOUCHER A CES ROUTES, ELLES SONT UTILISÉES HORS UI
-  '/download/fichiers/:documentId': { newDownload: etapeDocumentDownload },
-  '/download/entrepriseDocuments/:documentId': { newDownload: entrepriseDocumentDownload },
-  '/download/activiteDocuments/:documentId': { newDownload: activiteDocumentDownload },
-  '/download/avisDocument/:etapeAvisId': { newDownload: avisDocumentDownload },
-  '/fichiers/:documentId': { newDownload: etapeDocumentDownload },
-  '/titres/:id': { download: titre },
-  '/titres': { download: titres },
-  '/titres_qgis': { download: titres },
-  '/demarches': { download: demarches },
-  '/travaux': { download: travaux },
-  '/activites': { download: activites },
-  '/utilisateurs': { download: utilisateurs },
-  '/etape/zip/:etapeId': { download: etapeTelecharger },
-  '/entreprises': { download: entreprises },
+  '/download/fichiers/:documentId': { newDownloadCall: etapeDocumentDownload, ...CaminoRestRoutes['/download/fichiers/:documentId'] },
+  '/download/entrepriseDocuments/:documentId': { newDownloadCall: entrepriseDocumentDownload, ...CaminoRestRoutes['/download/entrepriseDocuments/:documentId'] },
+  '/download/activiteDocuments/:documentId': { newDownloadCall: activiteDocumentDownload, ...CaminoRestRoutes['/download/activiteDocuments/:documentId'] },
+  '/download/avisDocument/:etapeAvisId': { newDownloadCall: avisDocumentDownload, ...CaminoRestRoutes['/download/avisDocument/:etapeAvisId'] },
+  '/fichiers/:documentId': { newDownloadCall: etapeDocumentDownload, ...CaminoRestRoutes['/fichiers/:documentId'] },
+  '/titres/:id': { downloadCall: titre, ...CaminoRestRoutes['/titres/:id'] },
+  '/titres': { downloadCall: titres, ...CaminoRestRoutes['/titres'] },
+  '/titres_qgis': { downloadCall: titres, ...CaminoRestRoutes['/titres_qgis'] },
+  '/demarches': { downloadCall: demarches, ...CaminoRestRoutes['/demarches'] },
+  '/travaux': { downloadCall: travaux, ...CaminoRestRoutes['/travaux'] },
+  '/activites': { downloadCall: activites, ...CaminoRestRoutes['/activites'] },
+  '/utilisateurs': { downloadCall: utilisateurs, ...CaminoRestRoutes['/utilisateurs'] },
+  '/etape/zip/:etapeId': { downloadCall: etapeTelecharger, ...CaminoRestRoutes['/etape/zip/:etapeId'] },
+  '/entreprises': { downloadCall: entreprises, ...CaminoRestRoutes['/entreprises'] },
   // NE PAS TOUCHER A CES ROUTES, ELLES SONT UTILISÉES HORS UI
 
-  '/moi': { get: moi },
-  '/config': { get: getConfig },
-  '/rest/titres/:id/titreLiaisons': { get: getTitreLiaisons, post: postTitreLiaisons },
-  '/rest/etapesTypes/:demarcheId/:date': { get: getEtapesTypesEtapesStatusWithMainStep },
-  '/rest/titres': { post: titreDemandeCreer },
-  '/rest/titres/:titreId': { delete: removeTitre, post: updateTitre, get: getTitre },
-  '/rest/titres/:titreId/abonne': { post: utilisateurTitreAbonner, get: getUtilisateurTitreAbonner },
-  '/rest/titresONF': { get: titresONF },
-  '/rest/titresAdministrations': { get: titresAdministrations },
-  '/rest/statistiques/minerauxMetauxMetropole': { get: getMinerauxMetauxMetropolesStats }, // UNTESTED YET
-  '/rest/statistiques/guyane': { get: getGuyaneStats },
-  '/rest/statistiques/guyane/:annee': { get: getGuyaneStats },
-  '/rest/statistiques/granulatsMarins': { get: getGranulatsMarinsStats },
-  '/rest/statistiques/granulatsMarins/:annee': { get: getGranulatsMarinsStats },
-  '/rest/statistiques/dgtm': { get: getDGTMStats },
-  '/rest/statistiques/datagouv': { get: getDataGouvStats },
-  '/rest/demarches/:demarcheIdOrSlug': { get: getDemarcheByIdOrSlug },
-  '/rest/utilisateur/generateQgisToken': { post: generateQgisToken },
-  '/rest/utilisateurs/:id/permission': { post: updateUtilisateurPermission },
-  '/rest/utilisateurs/:id/delete': { get: deleteUtilisateur },
-  '/rest/utilisateurs/:id/newsletter': { get: isSubscribedToNewsletter, post: manageNewsletterSubscription }, // UNTESTED YET
-  '/rest/entreprises/:entrepriseId/fiscalite/:annee': { get: fiscalite }, // UNTESTED YET
-  '/rest/entreprises/:entrepriseId': { get: getEntreprise, put: modifierEntreprise },
-  '/rest/entreprises/:entrepriseId/documents': { get: getEntrepriseDocuments, post: postEntrepriseDocument },
-  '/rest/entreprises/:entrepriseId/documents/:entrepriseDocumentId': { delete: deleteEntrepriseDocument },
-  '/rest/entreprises': { post: creerEntreprise, get: getAllEntreprises },
-  '/rest/administrations/:administrationId/utilisateurs': { get: getAdministrationUtilisateurs },
-  '/rest/administrations/:administrationId/activiteTypeEmails': { get: getAdministrationActiviteTypeEmails, post: addAdministrationActiviteTypeEmails },
-  '/rest/administrations/:administrationId/activiteTypeEmails/delete': { post: deleteAdministrationActiviteTypeEmails },
-  '/rest/demarches/:demarcheId/geojson': { get: getPerimetreInfos },
-  '/rest/etapes/:etapeId/geojson': { get: getPerimetreInfos },
-  '/rest/etapes/:etapeIdOrSlug': { delete: deleteEtape, get: getEtape },
-  '/rest/etapes': { post: createEtape, put: updateEtape },
-  '/rest/etapes/:etapeId/depot': { put: deposeEtape },
-  '/rest/etapes/:etapeId/entrepriseDocuments': { get: getEtapeEntrepriseDocuments },
-  '/rest/etapes/:etapeId/etapeDocuments': { get: getEtapeDocuments },
-  '/rest/etapes/:etapeId/etapeAvis': { get: getEtapeAvis },
-  '/rest/activites/:activiteId': { get: getActivite, put: updateActivite, delete: deleteActivite },
-  '/rest/communes': { get: getCommunes },
-  '/rest/geojson/import/:geoSystemeId': { post: geojsonImport },
-  '/rest/geojson_points/import/:geoSystemeId': { post: geojsonImportPoints },
-  '/rest/geojson_forages/import/:geoSystemeId': { post: geojsonImportForages },
-  '/rest/geojson_points/:geoSystemeId': { post: convertGeojsonPointsToGeoSystemeId },
-  '/deconnecter': { get: logout },
-  '/changerMotDePasse': { get: resetPassword },
+  '/moi': { getCall: moi, ...CaminoRestRoutes['/moi'] },
+  '/config': { getCall: getConfig, ...CaminoRestRoutes['/config'] },
+  '/rest/titres/:id/titreLiaisons': { getCall: getTitreLiaisons, postCall: postTitreLiaisons, ...CaminoRestRoutes['/rest/titres/:id/titreLiaisons'] },
+  '/rest/etapesTypes/:demarcheId/:date': { getCall: getEtapesTypesEtapesStatusWithMainStep, ...CaminoRestRoutes['/rest/etapesTypes/:demarcheId/:date'] },
+  '/rest/titres': { postCall: titreDemandeCreer, ...CaminoRestRoutes['/rest/titres'] },
+  '/rest/titres/:titreId': { deleteCall: removeTitre, postCall: updateTitre, getCall: getTitre, ...CaminoRestRoutes['/rest/titres/:titreId'] },
+  '/rest/titres/:titreId/abonne': { postCall: utilisateurTitreAbonner, getCall: getUtilisateurTitreAbonner, ...CaminoRestRoutes['/rest/titres/:titreId/abonne'] },
+  '/rest/titresONF': { getCall: titresONF, ...CaminoRestRoutes['/rest/titresONF'] },
+  '/rest/titresAdministrations': { getCall: titresAdministrations, ...CaminoRestRoutes['/rest/titresAdministrations'] },
+  '/rest/statistiques/minerauxMetauxMetropole': { getCall: getMinerauxMetauxMetropolesStats, ...CaminoRestRoutes['/rest/statistiques/minerauxMetauxMetropole'] }, // UNTESTED YET
+  '/rest/statistiques/guyane': { getCall: getGuyaneStats, ...CaminoRestRoutes['/rest/statistiques/guyane'] },
+  '/rest/statistiques/guyane/:annee': { getCall: getGuyaneStats, ...CaminoRestRoutes['/rest/statistiques/guyane/:annee'] },
+  '/rest/statistiques/granulatsMarins': { getCall: getGranulatsMarinsStats, ...CaminoRestRoutes['/rest/statistiques/granulatsMarins'] },
+  '/rest/statistiques/granulatsMarins/:annee': { getCall: getGranulatsMarinsStats, ...CaminoRestRoutes['/rest/statistiques/granulatsMarins/:annee'] },
+  '/rest/statistiques/dgtm': { getCall: getDGTMStats, ...CaminoRestRoutes['/rest/statistiques/dgtm'] },
+  '/rest/statistiques/datagouv': { getCall: getDataGouvStats, ...CaminoRestRoutes['/rest/statistiques/datagouv'] },
+  '/rest/demarches/:demarcheIdOrSlug': { getCall: getDemarcheByIdOrSlug, ...CaminoRestRoutes['/rest/demarches/:demarcheIdOrSlug'] },
+  '/rest/utilisateur/generateQgisToken': { postCall: generateQgisToken, ...CaminoRestRoutes['/rest/utilisateur/generateQgisToken'] },
+  '/rest/utilisateurs/:id/permission': { postCall: updateUtilisateurPermission, ...CaminoRestRoutes['/rest/utilisateurs/:id/permission'] },
+  '/rest/utilisateurs/:id/delete': { getCall: deleteUtilisateur, ...CaminoRestRoutes['/rest/utilisateurs/:id/delete'] },
+  '/rest/utilisateurs/:id/newsletter': { getCall: isSubscribedToNewsletter, postCall: manageNewsletterSubscription, ...CaminoRestRoutes['/rest/utilisateurs/:id/newsletter'] }, // UNTESTED YET
+  '/rest/entreprises/:entrepriseId/fiscalite/:annee': { getCall: fiscalite, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId/fiscalite/:annee'] }, // UNTESTED YET
+  '/rest/entreprises/:entrepriseId': { getCall: getEntreprise, putCall: modifierEntreprise, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId'] },
+  '/rest/entreprises/:entrepriseId/documents': { getCall: getEntrepriseDocuments, postCall: postEntrepriseDocument, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId/documents'] },
+  '/rest/entreprises/:entrepriseId/documents/:entrepriseDocumentId': { deleteCall: deleteEntrepriseDocument, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId/documents/:entrepriseDocumentId'] },
+  '/rest/entreprises': { postCall: creerEntreprise, getCall: getAllEntreprises, ...CaminoRestRoutes['/rest/entreprises'] },
+  '/rest/administrations/:administrationId/utilisateurs': { getCall: getAdministrationUtilisateurs, ...CaminoRestRoutes['/rest/administrations/:administrationId/utilisateurs'] },
+  '/rest/administrations/:administrationId/activiteTypeEmails': {
+    getCall: getAdministrationActiviteTypeEmails,
+    newPostCall: addAdministrationActiviteTypeEmails,
+    ...CaminoRestRoutes['/rest/administrations/:administrationId/activiteTypeEmails'],
+  },
+  '/rest/administrations/:administrationId/activiteTypeEmails/delete': {
+    newPostCall: deleteAdministrationActiviteTypeEmails,
+    ...CaminoRestRoutes['/rest/administrations/:administrationId/activiteTypeEmails/delete'],
+  },
+  '/rest/demarches/:demarcheId/geojson': { getCall: getPerimetreInfos, ...CaminoRestRoutes['/rest/demarches/:demarcheId/geojson'] },
+  '/rest/etapes/:etapeId/geojson': { getCall: getPerimetreInfos, ...CaminoRestRoutes['/rest/etapes/:etapeId/geojson'] },
+  '/rest/etapes/:etapeIdOrSlug': { deleteCall: deleteEtape, getCall: getEtape, ...CaminoRestRoutes['/rest/etapes/:etapeIdOrSlug'] },
+  '/rest/etapes': { postCall: createEtape, putCall: updateEtape, ...CaminoRestRoutes['/rest/etapes'] },
+  '/rest/etapes/:etapeId/depot': { putCall: deposeEtape, ...CaminoRestRoutes['/rest/etapes/:etapeId/depot'] },
+  '/rest/etapes/:etapeId/entrepriseDocuments': { getCall: getEtapeEntrepriseDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/entrepriseDocuments'] },
+  '/rest/etapes/:etapeId/etapeDocuments': { getCall: getEtapeDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeDocuments'] },
+  '/rest/etapes/:etapeId/etapeAvis': { getCall: getEtapeAvis, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeAvis'] },
+  '/rest/activites/:activiteId': { getCall: getActivite, putCall: updateActivite, deleteCall: deleteActivite, ...CaminoRestRoutes['/rest/activites/:activiteId'] },
+  '/rest/communes': { getCall: getCommunes, ...CaminoRestRoutes['/rest/communes'] },
+  '/rest/geojson/import/:geoSystemeId': { newPostCall: geojsonImport, ...CaminoRestRoutes['/rest/geojson/import/:geoSystemeId'] },
+  '/rest/geojson_points/import/:geoSystemeId': { newPostCall: geojsonImportPoints, ...CaminoRestRoutes['/rest/geojson_points/import/:geoSystemeId'] },
+  '/rest/geojson_forages/import/:geoSystemeId': { newPostCall: geojsonImportForages, ...CaminoRestRoutes['/rest/geojson_forages/import/:geoSystemeId'] },
+  '/deconnecter': { getCall: logout, ...CaminoRestRoutes['/deconnecter'] },
+  '/changerMotDePasse': { getCall: resetPassword, ...CaminoRestRoutes['/changerMotDePasse'] },
 } as const
-
 export const restWithPool = (dbPool: Pool) => {
   const rest = express.Router()
 
@@ -179,32 +196,87 @@ export const restWithPool = (dbPool: Pool) => {
     .filter(isCaminoRestRoute)
     .forEach(route => {
       const maRoute = restRouteImplementations[route]
-      if ('get' in maRoute) {
+      if ('getCall' in maRoute) {
         console.info(`GET ${route}`)
-        rest.get(route, restCatcher(maRoute.get(dbPool)))
+        rest.get(route, restCatcher(maRoute.getCall(dbPool)))
       }
-      if ('post' in maRoute) {
+      if ('postCall' in maRoute) {
         console.info(`POST ${route}`)
-        rest.post(route, restCatcherWithMutation('post', maRoute.post(dbPool), dbPool))
+        rest.post(route, restCatcherWithMutation('post', maRoute.postCall(dbPool), dbPool))
       }
-      if ('put' in maRoute) {
+
+      if ('newPostCall' in maRoute) {
+        console.info(`POST ${route}`)
+        rest.post(route, async (req: CaminoRequest, res: express.Response, _next: express.NextFunction) => {
+          try {
+            const call = pipe(
+              TE.Do,
+              TE.bindW('user', () => {
+                if (isNotNullNorUndefined(req.auth)) {
+                  return TE.right(req.auth as UserNotNull)
+                } else {
+                  return TE.left({ message: 'Accès interdit', status: HTTP_STATUS.HTTP_STATUS_FORBIDDEN })
+                }
+              }),
+              TE.bindW('body', () => zodParseTaskEither(maRoute.newPost.input, req.body)),
+              TE.bindW('params', () => zodParseTaskEither(maRoute.params, req.params)),
+              TE.mapLeft(caminoError => {
+                if (!('status' in caminoError)) {
+                  return { ...caminoError, status: HTTP_STATUS.HTTP_STATUS_BAD_REQUEST }
+                }
+
+                return caminoError
+              }),
+              // TODO 2024-06-26 ici, si on ne met pas le body et params à any, on se retrouve avec une typescript union hell qui fait tout planter
+              TE.bindW<'result', { body: any; user: UserNotNull; params: any }, CaminoApiError<string>, DeepReadonly<z.infer<(typeof maRoute)['newPost']['output']>>>(
+                'result',
+                ({ user, body, params }) => maRoute.newPostCall(dbPool, user, body, params)
+              ),
+              TE.bindW('parsedResult', ({ result }) =>
+                pipe(
+                  zodParseTaskEither(maRoute.newPost.output, result),
+                  TE.mapLeft(caminoError => ({ ...caminoError, status: HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR }))
+                )
+              ),
+              TE.mapBoth(
+                caminoError => {
+                  console.warn(`problem with route ${route}: ${caminoError.message}`)
+                  res.status(caminoError.status).json(caminoError)
+                },
+                ({ parsedResult, user }) => {
+                  res.json(parsedResult)
+
+                  return addLog(dbPool, user.id, 'post', req.url, req.body)
+                }
+              )
+            )
+
+            await call()
+          } catch (e) {
+            console.error('catching error on newPost route', route, e, req.body)
+            res.status(HTTP_STATUS.HTTP_STATUS_INTERNAL_SERVER_ERROR).json({ message: "une erreur inattendue s'est produite", extra: e })
+          }
+        })
+      }
+
+      if ('putCall' in maRoute) {
         console.info(`PUT ${route}`)
-        rest.put(route, restCatcherWithMutation('put', maRoute.put(dbPool), dbPool))
+        rest.put(route, restCatcherWithMutation('put', maRoute.putCall(dbPool), dbPool))
       }
 
-      if ('delete' in maRoute) {
+      if ('deleteCall' in maRoute) {
         console.info(`delete ${route}`)
-        rest.delete(route, restCatcherWithMutation('delete', maRoute.delete(dbPool), dbPool))
+        rest.delete(route, restCatcherWithMutation('delete', maRoute.deleteCall(dbPool), dbPool))
       }
 
-      if ('download' in maRoute) {
+      if ('downloadCall' in maRoute) {
         console.info(`download ${route}`)
-        rest.get(route, restDownload(maRoute.download(dbPool)))
+        rest.get(route, restDownload(maRoute.downloadCall(dbPool)))
       }
 
-      if ('newDownload' in maRoute) {
+      if ('newDownloadCall' in maRoute) {
         console.info(`newDownload ${route}`)
-        rest.get(route, restNewDownload(dbPool, maRoute.newDownload))
+        rest.get(route, restNewDownload(dbPool, maRoute.newDownloadCall))
       }
     })
 
@@ -239,7 +311,7 @@ const restCatcherWithMutation = (method: string, expressCall: ExpressRoute, pool
       res.sendStatus(HTTP_STATUS.HTTP_STATUS_FORBIDDEN)
     } else {
       await expressCall(req, res, next)
-      await addLog(pool, user.id, method, req.url, req.body)
+      await addLog(pool, user.id, method, req.url, req.body)()
     }
   } catch (e) {
     console.error('catching error', e)
diff --git a/packages/api/src/tools/fp-tools.ts b/packages/api/src/tools/fp-tools.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3db5472f76d5d1f9651522338a803131986f8e67
--- /dev/null
+++ b/packages/api/src/tools/fp-tools.ts
@@ -0,0 +1,27 @@
+import { CaminoError, CaminoZodErrorReadableMessage } from 'camino-common/src/zod-tools.js'
+import E from 'fp-ts/lib/Either.js'
+import TE from 'fp-ts/lib/TaskEither.js'
+import { ZodTypeAny } from 'zod'
+import { fromError } from 'zod-validation-error'
+
+export type ZodUnparseable = 'Problème de validation de données'
+export const zodParseEitherCallback =
+  <T extends ZodTypeAny>(validator: T) =>
+  (value: unknown): E.Either<CaminoError<ZodUnparseable>, T['_output']> =>
+    zodParseEither(validator, value)
+
+export const zodParseTaskEitherCallback =
+  <T extends ZodTypeAny>(validator: T) =>
+  (value: unknown): TE.TaskEither<CaminoError<ZodUnparseable>, T['_output']> =>
+    zodParseTaskEither(validator, value)
+
+export const zodParseEither = <T extends ZodTypeAny>(validator: T, item: unknown): E.Either<CaminoError<ZodUnparseable>, T['_output']> => {
+  return E.tryCatch(
+    () => validator.parse(item),
+    myError => ({ message: 'Problème de validation de données', zodErrorReadableMessage: fromError(myError).toString() as CaminoZodErrorReadableMessage })
+  )
+}
+
+export const zodParseTaskEither = <T extends ZodTypeAny>(validator: T, item: unknown): TE.TaskEither<CaminoError<ZodUnparseable>, T['_output']> => {
+  return TE.fromEither(zodParseEither(validator, item))
+}
diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts
index 7e58fd0708040931aea93ffd4070c2cdfd13b51d..1980dd28781d4f6b951bbd6bb8c740c187b63596 100644
--- a/packages/api/src/types.ts
+++ b/packages/api/src/types.ts
@@ -29,6 +29,8 @@ import { EtapeHeritageProps } from 'camino-common/src/heritage'
 import { GeoSystemeId } from 'camino-common/src/static/geoSystemes'
 import { ElementWithValue } from 'camino-common/src/sections'
 import { KM2 } from 'camino-common/src/number'
+import { HttpStatus } from 'camino-common/src/http'
+import { CaminoError } from 'camino-common/src/zod-tools'
 
 const TitreEtapesTravauxTypes = {
   AvisDesServicesEtCommissionsConsultatives: 'asc',
@@ -63,7 +65,7 @@ const TitreEtapesTravauxTypes = {
   DecisionAdmin: 'wdm',
   PorterAConnaissance: 'wpb',
 } as const satisfies Record<string, EtapeTypeId>
-
+export type CaminoApiError<T extends string> = CaminoError<T> & { status: HttpStatus }
 export interface IFields {
   [key: string]: IFields
 }
@@ -174,9 +176,9 @@ interface ITitre {
   administrationsLocales?: AdministrationId[] | null
   administrations?: AdministrationId[] | null
   surface?: number | null
-  communes?: ICommune[] | null
-  forets?: ForetId[] | null
-  sdomZones?: SDOMZoneId[] | null
+  communes?: DeepReadonly<ICommune[]> | null
+  forets?: DeepReadonly<ForetId[]> | null
+  sdomZones?: DeepReadonly<SDOMZoneId[]> | null
   pointsEtape?: ITitreEtape | null
   secteursMaritime?: SecteursMaritimes[] | null
   demarches?: ITitreDemarche[]
@@ -255,9 +257,9 @@ type ITitreEtape = {
   administrationsLocales?: AdministrationId[] | null
   entrepriseDocumentIds?: EntrepriseDocumentId[] | null
   etapeDocuments?: unknown[]
-  communes?: ICommune[] | null
-  forets?: ForetId[] | null
-  sdomZones?: SDOMZoneId[] | null
+  communes?: DeepReadonly<ICommune[]> | null
+  forets?: DeepReadonly<ForetId[]> | null
+  sdomZones?: DeepReadonly<SDOMZoneId[]> | null
   secteursMaritime?: SecteursMaritimes[] | null
   heritageProps?: IHeritageProps | null
   heritageContenu?: IHeritageContenu | null
diff --git a/packages/api/tests/_utils/index.ts b/packages/api/tests/_utils/index.ts
index 122afda5d81fd2fe0a5f1cb704059b38704c8321..7442b45e40f433f2263d5129e1c83db991974062 100644
--- a/packages/api/tests/_utils/index.ts
+++ b/packages/api/tests/_utils/index.ts
@@ -11,7 +11,7 @@ import { userSuper } from '../../src/database/user-super'
 import { AdminUserNotNull, isAdministrationRole, isSuperRole, UserNotNull } from 'camino-common/src/roles.js'
 import { TestUser } from 'camino-common/src/tests-utils.js'
 import { getCurrent } from 'camino-common/src/date.js'
-import { CaminoRestRoutes, DeleteRestRoutes, getRestRoute, GetRestRoutes, PostRestRoutes, PutRestRoutes, CaminoRestParams, DownloadRestRoutes } from 'camino-common/src/rest.js'
+import { CaminoRestRoutes, DeleteRestRoutes, getRestRoute, GetRestRoutes, PostRestRoutes, PutRestRoutes, CaminoRestParams, DownloadRestRoutes, NewPostRestRoutes } from 'camino-common/src/rest.js'
 import { z } from 'zod'
 import { newUtilisateurId } from '../../src/database/models/_format/id-create.js'
 import { idUserKeycloakRecognised } from '../keycloak.js'
@@ -71,6 +71,19 @@ export const restPostCall = async <Route extends PostRestRoutes>(
 
   return jwtSet(req, user)
 }
+export const restNewPostCall = async <Route extends NewPostRestRoutes>(
+  pool: Pool,
+  caminoRestRoute: Route,
+  params: CaminoRestParams<Route>,
+  user: TestUser | undefined,
+  body: DeepReadonly<z.infer<(typeof CaminoRestRoutes)[Route]['newPost']['input']>>
+): Promise<request.Test> => {
+  const req = request(app(pool))
+    .post(getRestRoute(caminoRestRoute, params))
+    .send(body ?? undefined)
+
+  return jwtSet(req, user)
+}
 
 export const restPutCall = async <Route extends PutRestRoutes>(
   pool: Pool,
diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json
index 32a128bf4641ba5b5e87f473fe755eb9978e4bdb..34ab260f8d7ca6b57750b682e1457233a48beadb 100644
--- a/packages/api/tsconfig.json
+++ b/packages/api/tsconfig.json
@@ -8,7 +8,7 @@
     "alwaysStrict": true,
     "esModuleInterop": true,
     "inlineSources": false,
-    "lib": ["es2020", "dom"],
+    "lib": ["es2020", "dom", "ESNext.Array"],
     "module": "ES2020",
     "moduleResolution": "Bundler",
     "noFallthroughCasesInSwitch": true,
diff --git a/packages/common/src/number.ts b/packages/common/src/number.ts
index d10f20bb2231e75de5d1f8d8e10ddf2c858fb834..61606c9e61aadd12a399e1628d6031db2d60bab1 100644
--- a/packages/common/src/number.ts
+++ b/packages/common/src/number.ts
@@ -1,4 +1,4 @@
-import { z } from 'zod'
+import { z, ZodNumber } from 'zod'
 
 export const numberFormat = (number: number): string =>
   Intl.NumberFormat('FR-fr', {
@@ -34,10 +34,9 @@ export const toDegresMinutes = (value: number): { degres: number; minutes: numbe
 }
 
 export const km2Validator = z.number().nonnegative().brand('CAMINO_KM2')
-export const m2Validator = z
-  .number()
-  .transform(value => parseInt(`${value}`))
-  .brand('CAMINO_M2')
+export const createM2Validator = (v: ZodNumber) => v.transform(value => parseInt(`${value}`)).brand('CAMINO_M2')
+export const m2Validator = createM2Validator(z.number())
+
 export const ZERO_KM2 = km2Validator.parse(0)
 export type KM2 = z.infer<typeof km2Validator>
 export type M2 = z.infer<typeof m2Validator>
diff --git a/packages/common/src/perimetre.ts b/packages/common/src/perimetre.ts
index 226ac0e247f9081364b92da2447f68be465ce09c..6d7b0b88167e701de41e27a62374d7532340a3eb 100644
--- a/packages/common/src/perimetre.ts
+++ b/packages/common/src/perimetre.ts
@@ -8,7 +8,7 @@ import { titreSlugValidator } from './validators/titres.js'
 import { tempDocumentNameValidator } from './document.js'
 import { titreTypeIdValidator } from './static/titresTypes.js'
 import { perimetreFileUploadTypeValidator } from './static/documentsTypes.js'
-import { isNullOrUndefined } from './typescript-tools.js'
+import { DeepReadonly, isNullOrUndefined } from './typescript-tools.js'
 import { km2Validator } from './number.js'
 import { GeoSystemeId, geoSystemeIdValidator } from './static/geoSystemes.js'
 
@@ -132,7 +132,7 @@ export const geojsonInformationsValidator = z.object({
   geojson_origine_geo_systeme_id: geoSystemeIdValidator,
 })
 
-export type GeojsonInformations = z.infer<typeof geojsonInformationsValidator>
+export type GeojsonInformations = DeepReadonly<z.infer<typeof geojsonInformationsValidator>>
 
 export const perimetreInformationsValidator = geojsonInformationsValidator.pick({ superposition_alertes: true, sdomZoneIds: true }).extend({ communes: z.array(communeIdValidator) })
 export type PerimetreInformations = z.infer<typeof perimetreInformationsValidator>
diff --git a/packages/common/src/permissions/administrations.ts b/packages/common/src/permissions/administrations.ts
index fa40d0e6a60ddfeb25be19c0a7b896ce91809075..e5269548979a0afd91f44423310c48a88f51c234 100644
--- a/packages/common/src/permissions/administrations.ts
+++ b/packages/common/src/permissions/administrations.ts
@@ -1,6 +1,7 @@
 import { isAdministration, isAdministrationAdmin, isAdministrationEditeur, isSuper, User } from '../roles.js'
 import { AdministrationId, Administrations, sortedAdministrations } from '../static/administrations.js'
 import { Departements } from '../static/departement.js'
+import { DeepReadonly } from '../typescript-tools.js'
 
 export const canReadActivitesTypesEmails = (user: User, administrationId: AdministrationId) => {
   if (!canReadAdministrations(user)) {
@@ -31,7 +32,7 @@ export const canReadActivitesTypesEmails = (user: User, administrationId: Admini
   return false
 }
 
-export const canReadAdministrations = (user: User) => isSuper(user) || isAdministration(user)
+export const canReadAdministrations = (user: DeepReadonly<User>) => isSuper(user) || isAdministration(user)
 
 export const canEditEmails = (user: User, administrationId: AdministrationId): boolean => {
   if (isSuper(user) || ((isAdministrationAdmin(user) || isAdministrationEditeur(user)) && Administrations[user.administrationId].typeId === 'min')) {
diff --git a/packages/common/src/permissions/titres.ts b/packages/common/src/permissions/titres.ts
index 99aba1ee681d881a4fdb224318357d0e4b6027bd..cb4bc00adc9f047bb4ec648f9c2f33771e49a92d 100644
--- a/packages/common/src/permissions/titres.ts
+++ b/packages/common/src/permissions/titres.ts
@@ -10,7 +10,7 @@ import { activitesTypesPays } from '../static/activitesTypesPays.js'
 import { canAdministrationModifyTitres } from '../static/administrationsTitresTypesTitresStatuts.js'
 import { TitreStatutId } from '../static/titresStatuts.js'
 import { territoiresIdFind } from '../territoires.js'
-import { isNotNullNorUndefinedNorEmpty, isNullOrUndefinedOrEmpty, SimplePromiseFn } from '../typescript-tools.js'
+import { DeepReadonly, isNotNullNorUndefinedNorEmpty, isNullOrUndefinedOrEmpty, SimplePromiseFn } from '../typescript-tools.js'
 import { SecteursMaritimes } from '../static/facades.js'
 import { EntrepriseId } from '../entreprise.js'
 
@@ -101,7 +101,7 @@ export const canDeleteTitre = (user: User): boolean => isSuper(user)
 
 interface TitreReduced {
   titreTypeId: TitreTypeId
-  communes: { id: CommuneId }[]
+  communes: DeepReadonly<{ id: CommuneId }[]>
   secteursMaritime: SecteursMaritimes[]
   demarches: unknown[]
 }
diff --git a/packages/common/src/rest.test.ts b/packages/common/src/rest.test.ts
index 6e52f536e8f06aca6045f98011e64e7c9d5a2218..f4ff44ffe577e1f22ede4a965fd8a5de964a702f 100644
--- a/packages/common/src/rest.test.ts
+++ b/packages/common/src/rest.test.ts
@@ -1,8 +1,11 @@
 import { test, expect } from 'vitest'
 import { getRestRoute } from './rest'
+import { utilisateurIdValidator } from './roles'
 
 test('getRestRoute', () => {
-  expect(getRestRoute('/rest/utilisateurs/:id/newsletter', { id: 'userId' })).toBe('/rest/utilisateurs/userId/newsletter')
-  expect(getRestRoute('/rest/utilisateurs/:id/newsletter', { id: 'userId' }, { toto: ['plop', 'plip'] })).toBe('/rest/utilisateurs/userId/newsletter?toto%5B%5D=plop&toto%5B%5D=plip')
-  expect(getRestRoute('/rest/utilisateurs/:id/newsletter', { id: 'userId' }, { toto: 'plop' })).toBe('/rest/utilisateurs/userId/newsletter?toto=plop')
+  expect(getRestRoute('/rest/utilisateurs/:id/newsletter', { id: utilisateurIdValidator.parse('userId') })).toBe('/rest/utilisateurs/userId/newsletter')
+  expect(getRestRoute('/rest/utilisateurs/:id/newsletter', { id: utilisateurIdValidator.parse('userId') }, { toto: ['plop', 'plip'] })).toBe(
+    '/rest/utilisateurs/userId/newsletter?toto%5B%5D=plop&toto%5B%5D=plip'
+  )
+  expect(getRestRoute('/rest/utilisateurs/:id/newsletter', { id: utilisateurIdValidator.parse('userId') }, { toto: 'plop' })).toBe('/rest/utilisateurs/userId/newsletter?toto=plop')
 })
diff --git a/packages/common/src/rest.ts b/packages/common/src/rest.ts
index 45ade49f55424fd1381bb4cc8e582e4d83646f23..bbfac2395226d3d8599b0676dff2374cae8d3c8a 100644
--- a/packages/common/src/rest.ts
+++ b/packages/common/src/rest.ts
@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/ban-types */
-import { ZodType, z } from 'zod'
+import { ZodObject, ZodType, ZodTypeAny, z } from 'zod'
 import {
   entrepriseDocumentIdValidator,
   entrepriseDocumentInputValidator,
@@ -24,7 +24,7 @@ import {
   titreOnfValidator,
   utilisateurTitreAbonneValidator,
 } from './titres.js'
-import { adminUserNotNullValidator, userValidator } from './roles.js'
+import { adminUserNotNullValidator, userValidator, utilisateurIdValidator } from './roles.js'
 import { caminoAnneeValidator, caminoDateValidator } from './date.js'
 import {
   etapeDocumentIdValidator,
@@ -49,7 +49,6 @@ import { Expect, isFalse, isTrue } from './typescript-tools.js'
 import { activiteDocumentIdValidator, activiteEditionValidator, activiteIdOrSlugValidator, activiteValidator } from './activite.js'
 import { geoSystemeIdValidator } from './static/geoSystemes.js'
 import {
-  featureCollectionPointsValidator,
   geojsonImportBodyValidator,
   geojsonImportForagesBodyValidator,
   geojsonImportForagesResponseValidator,
@@ -63,9 +62,10 @@ import { administrationIdValidator } from './static/administrations.js'
 import { administrationActiviteTypeEmailValidator } from './administrations.js'
 import { flattenEtapeValidator, restEtapeCreationValidator, restEtapeModificationValidator } from './etape-form.js'
 
-type CaminoRoute<T extends string> = (keyof ZodParseUrlParams<T> extends never ? {} : { params: ZodParseUrlParams<T> }) & {
+type CaminoRoute<T extends string> = { params: ZodObjectParsUrlParams<T> } & {
   get?: { output: ZodType }
   post?: { input: ZodType; output: ZodType }
+  newPost?: { input: ZodType; output: ZodType }
   put?: { input: ZodType; output: ZodType }
   delete?: true
   download?: true
@@ -112,7 +112,6 @@ const IDS = [
   '/rest/etapes',
   '/rest/activites/:activiteId',
   '/rest/geojson/import/:geoSystemeId',
-  '/rest/geojson_points/:geoSystemeId',
   '/rest/geojson_points/import/:geoSystemeId',
   '/rest/geojson_forages/import/:geoSystemeId',
   '/rest/communes',
@@ -137,95 +136,104 @@ const IDS = [
 ] as const
 
 export type CaminoRestRoute = (typeof IDS)[number]
-
+const noParamsValidator = z.object({})
+const utilisateurIdParamsValidator = z.object({ id: utilisateurIdValidator })
+const entrepriseIdParamsValidator = z.object({ entrepriseId: entrepriseIdValidator })
+const etapeIdParamsValidator = z.object({ etapeId: etapeIdValidator })
+const administrationIdParamsValidator = z.object({ administrationId: administrationIdValidator })
+const geoSystemIdParamsValidator = z.object({ geoSystemeId: geoSystemeIdValidator })
 export const CaminoRestRoutes = {
-  '/config': { get: { output: caminoConfigValidator } },
-  '/moi': { get: { output: userValidator } },
-  '/rest/utilisateurs/:id/newsletter': { params: { id: z.string() }, get: { output: z.boolean() }, post: { input: newsletterAbonnementValidator, output: z.boolean() } },
+  '/config': { params: noParamsValidator, get: { output: caminoConfigValidator } },
+  '/moi': { params: noParamsValidator, get: { output: userValidator } },
+  '/rest/utilisateurs/:id/newsletter': { params: utilisateurIdParamsValidator, get: { output: z.boolean() }, post: { input: newsletterAbonnementValidator, output: z.boolean() } },
   // On passe par un http get plutot qu'un http delete car nous terminons par une redirection vers la deconnexion de oauth2, qui se traduit mal sur certains navigateurs et essaie de faire un delete sur une route get
-  '/rest/utilisateurs/:id/delete': { params: { id: z.string() }, get: { output: z.void() } },
-  '/rest/utilisateurs/:id/permission': { params: { id: z.string() }, post: { input: utilisateurToEdit, output: z.void() } },
-  '/rest/statistiques/minerauxMetauxMetropole': { get: { output: statistiquesMinerauxMetauxMetropoleValidator } },
-  '/rest/statistiques/guyane': { get: { output: statistiquesGuyaneDataValidator } },
-  '/rest/statistiques/guyane/:annee': { params: { annee: caminoAnneeValidator }, get: { output: statistiquesGuyaneDataValidator } },
-  '/rest/statistiques/granulatsMarins': { get: { output: statistiquesGranulatsMarinsValidator } },
-  '/rest/statistiques/granulatsMarins/:annee': { params: { annee: caminoAnneeValidator }, get: { output: statistiquesGranulatsMarinsValidator } },
-  '/rest/statistiques/datagouv': { get: { output: z.array(statistiquesDataGouvValidator) } },
-  '/rest/titres': { post: { input: titreDemandeValidator, output: titreDemandeOutputValidator } },
-  '/rest/titres/:titreId': { params: { titreId: titreIdOrSlugValidator }, get: { output: titreGetValidator }, delete: true, post: { output: z.void(), input: editableTitreValidator } },
-  '/rest/titres/:titreId/abonne': { params: { titreId: titreIdValidator }, post: { input: utilisateurTitreAbonneValidator, output: z.void() }, get: { output: z.boolean() } },
-  '/rest/titresONF': { get: { output: z.array(titreOnfValidator) } },
-  '/rest/titresAdministrations': { get: { output: z.array(titreAdministrationValidator) } },
-  '/rest/titres/:id/titreLiaisons': { params: { id: titreIdValidator }, get: { output: titreLinksValidator }, post: { input: z.array(z.string()), output: titreLinksValidator } },
-  '/rest/demarches/:demarcheIdOrSlug': { params: { demarcheIdOrSlug: demarcheIdOrSlugValidator }, get: { output: getDemarcheByIdOrSlugValidator } },
+  '/rest/utilisateurs/:id/delete': { params: utilisateurIdParamsValidator, get: { output: z.void() } },
+  '/rest/utilisateurs/:id/permission': { params: utilisateurIdParamsValidator, post: { input: utilisateurToEdit, output: z.void() } },
+  '/rest/statistiques/minerauxMetauxMetropole': { params: noParamsValidator, get: { output: statistiquesMinerauxMetauxMetropoleValidator } },
+  '/rest/statistiques/guyane': { params: noParamsValidator, get: { output: statistiquesGuyaneDataValidator } },
+  '/rest/statistiques/guyane/:annee': { params: z.object({ annee: caminoAnneeValidator }), get: { output: statistiquesGuyaneDataValidator } },
+  '/rest/statistiques/granulatsMarins': { params: noParamsValidator, get: { output: statistiquesGranulatsMarinsValidator } },
+  '/rest/statistiques/granulatsMarins/:annee': { params: z.object({ annee: caminoAnneeValidator }), get: { output: statistiquesGranulatsMarinsValidator } },
+  '/rest/statistiques/datagouv': { params: noParamsValidator, get: { output: z.array(statistiquesDataGouvValidator) } },
+  '/rest/titres': { params: noParamsValidator, post: { 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() }, get: { output: z.boolean() } },
+  '/rest/titresONF': { params: noParamsValidator, get: { output: z.array(titreOnfValidator) } },
+  '/rest/titresAdministrations': { params: noParamsValidator, get: { output: z.array(titreAdministrationValidator) } },
+  '/rest/titres/:id/titreLiaisons': { params: z.object({ id: titreIdValidator }), get: { output: titreLinksValidator }, post: { input: z.array(z.string()), output: titreLinksValidator } },
+  '/rest/demarches/:demarcheIdOrSlug': { params: z.object({ demarcheIdOrSlug: demarcheIdOrSlugValidator }), get: { output: getDemarcheByIdOrSlugValidator } },
 
-  '/rest/statistiques/dgtm': { get: { output: statistiquesDGTMValidator } },
+  '/rest/statistiques/dgtm': { params: noParamsValidator, get: { output: statistiquesDGTMValidator } },
 
-  '/rest/entreprises/:entrepriseId/fiscalite/:annee': { params: { entrepriseId: entrepriseIdValidator, annee: caminoAnneeValidator }, get: { output: fiscaliteValidator } },
-  '/rest/entreprises': { post: { input: z.object({ siren: sirenValidator }), output: z.void() }, get: { output: z.array(entrepriseValidator) } },
+  '/rest/entreprises/:entrepriseId/fiscalite/:annee': { params: z.object({ entrepriseId: entrepriseIdValidator, annee: caminoAnneeValidator }), get: { output: fiscaliteValidator } },
+  '/rest/entreprises': { params: noParamsValidator, post: { input: z.object({ siren: sirenValidator }), output: z.void() }, get: { output: z.array(entrepriseValidator) } },
   '/rest/entreprises/:entrepriseId': {
-    params: { entrepriseId: entrepriseIdValidator },
+    params: entrepriseIdParamsValidator,
     get: { output: entrepriseTypeValidator },
     put: { input: entrepriseModificationValidator, output: z.void() },
   },
   '/rest/entreprises/:entrepriseId/documents': {
-    params: { entrepriseId: entrepriseIdValidator },
+    params: entrepriseIdParamsValidator,
     // TODO 2024-01-31 ne pas retourner une erreur, mais thrower une exception et la catcher plutôt ?
     post: { input: entrepriseDocumentInputValidator, output: z.union([entrepriseDocumentIdValidator, z.custom<Error>()]) },
     get: { output: z.array(entrepriseDocumentValidator) },
   },
-  '/rest/entreprises/:entrepriseId/documents/:entrepriseDocumentId': { params: { entrepriseId: entrepriseIdValidator, entrepriseDocumentId: entrepriseDocumentIdValidator }, delete: true },
-  '/rest/administrations/:administrationId/utilisateurs': { params: { administrationId: administrationIdValidator }, get: { output: z.array(adminUserNotNullValidator) } },
+  '/rest/entreprises/:entrepriseId/documents/:entrepriseDocumentId': { params: z.object({ entrepriseId: entrepriseIdValidator, entrepriseDocumentId: entrepriseDocumentIdValidator }), delete: true },
+  '/rest/administrations/:administrationId/utilisateurs': { params: administrationIdParamsValidator, get: { output: z.array(adminUserNotNullValidator) } },
   '/rest/administrations/:administrationId/activiteTypeEmails': {
-    params: { administrationId: administrationIdValidator },
+    params: administrationIdParamsValidator,
     get: { output: z.array(administrationActiviteTypeEmailValidator) },
-    post: { input: administrationActiviteTypeEmailValidator, output: z.boolean() },
+    newPost: { input: administrationActiviteTypeEmailValidator, output: z.boolean() },
   },
   '/rest/administrations/:administrationId/activiteTypeEmails/delete': {
-    params: { administrationId: administrationIdValidator },
-    post: { input: administrationActiviteTypeEmailValidator, output: z.boolean() },
+    params: administrationIdParamsValidator,
+    newPost: { input: administrationActiviteTypeEmailValidator, output: z.boolean() },
+  },
+  '/rest/utilisateur/generateQgisToken': { params: noParamsValidator, post: { input: z.void(), output: qgisTokenValidator } },
+  '/rest/etapesTypes/:demarcheId/:date': { params: z.object({ demarcheId: demarcheIdValidator, date: caminoDateValidator }), get: { output: z.array(etapeTypeEtapeStatutWithMainStepValidator) } },
+  '/rest/demarches/:demarcheId/geojson': { params: z.object({ demarcheId: demarcheIdOrSlugValidator }), get: { output: perimetreInformationsValidator } },
+  '/rest/etapes/:etapeId/geojson': { params: z.object({ etapeId: etapeIdOrSlugValidator }), get: { output: perimetreInformationsValidator } },
+  '/rest/etapes/:etapeId/etapeDocuments': { params: etapeIdParamsValidator, get: { output: getEtapeDocumentsByEtapeIdValidator } },
+  '/rest/etapes/:etapeId/etapeAvis': { params: etapeIdParamsValidator, get: { output: getEtapeAvisByEtapeIdValidator } },
+  '/rest/etapes/:etapeId/entrepriseDocuments': { params: etapeIdParamsValidator, get: { output: z.array(etapeEntrepriseDocumentValidator) } },
+  '/rest/etapes/:etapeIdOrSlug': { params: z.object({ etapeIdOrSlug: etapeIdOrSlugValidator }), delete: true, get: { output: flattenEtapeValidator } },
+  '/rest/etapes/:etapeId/depot': { params: etapeIdParamsValidator, put: { input: z.void(), output: z.void() } },
+  '/rest/etapes': { params: noParamsValidator, post: { input: restEtapeCreationValidator, output: etapeIdValidator }, put: { input: restEtapeModificationValidator, output: etapeIdValidator } },
+  '/rest/activites/:activiteId': {
+    params: z.object({ activiteId: activiteIdOrSlugValidator }),
+    get: { output: activiteValidator },
+    put: { input: activiteEditionValidator, output: z.void() },
+    delete: true,
   },
-  '/rest/utilisateur/generateQgisToken': { post: { input: z.void(), output: qgisTokenValidator } },
-  '/rest/etapesTypes/:demarcheId/:date': { params: { demarcheId: demarcheIdValidator, date: caminoDateValidator }, get: { output: z.array(etapeTypeEtapeStatutWithMainStepValidator) } },
-  '/rest/demarches/:demarcheId/geojson': { params: { demarcheId: demarcheIdOrSlugValidator }, get: { output: perimetreInformationsValidator } },
-  '/rest/etapes/:etapeId/geojson': { params: { etapeId: etapeIdOrSlugValidator }, get: { output: perimetreInformationsValidator } },
-  '/rest/etapes/:etapeId/etapeDocuments': { params: { etapeId: etapeIdValidator }, get: { output: getEtapeDocumentsByEtapeIdValidator } },
-  '/rest/etapes/:etapeId/etapeAvis': { params: { etapeId: etapeIdValidator }, get: { output: getEtapeAvisByEtapeIdValidator } },
-  '/rest/etapes/:etapeId/entrepriseDocuments': { params: { etapeId: etapeIdValidator }, get: { output: z.array(etapeEntrepriseDocumentValidator) } },
-  '/rest/etapes/:etapeIdOrSlug': { params: { etapeIdOrSlug: etapeIdOrSlugValidator }, delete: true, get: { output: flattenEtapeValidator } },
-  '/rest/etapes/:etapeId/depot': { params: { etapeId: etapeIdValidator }, put: { input: z.void(), output: z.void() } },
-  '/rest/etapes': { post: { input: restEtapeCreationValidator, output: etapeIdValidator }, put: { input: restEtapeModificationValidator, output: etapeIdValidator } },
-  '/rest/activites/:activiteId': { params: { activiteId: activiteIdOrSlugValidator }, get: { output: activiteValidator }, put: { input: activiteEditionValidator, output: z.void() }, delete: true },
-  '/rest/communes': { get: { output: z.array(communeValidator) } },
-  '/rest/geojson_points/:geoSystemeId': { params: { geoSystemeId: geoSystemeIdValidator }, post: { input: featureCollectionPointsValidator, output: featureCollectionPointsValidator } },
+  '/rest/communes': { params: noParamsValidator, get: { output: z.array(communeValidator) } },
   '/rest/geojson/import/:geoSystemeId': {
-    params: { geoSystemeId: geoSystemeIdValidator },
-    post: { input: geojsonImportBodyValidator, output: geojsonInformationsValidator },
+    params: geoSystemIdParamsValidator,
+    newPost: { input: geojsonImportBodyValidator, output: geojsonInformationsValidator },
   },
   '/rest/geojson_points/import/:geoSystemeId': {
-    params: { geoSystemeId: geoSystemeIdValidator },
-    post: { input: geojsonImportPointBodyValidator, output: geojsonImportPointResponseValidator },
+    params: geoSystemIdParamsValidator,
+    newPost: { input: geojsonImportPointBodyValidator, output: geojsonImportPointResponseValidator },
   },
   '/rest/geojson_forages/import/:geoSystemeId': {
-    params: { geoSystemeId: geoSystemeIdValidator },
-    post: { input: geojsonImportForagesBodyValidator, output: geojsonImportForagesResponseValidator },
+    params: geoSystemIdParamsValidator,
+    newPost: { input: geojsonImportForagesBodyValidator, output: geojsonImportForagesResponseValidator },
   },
-  '/deconnecter': { get: { output: z.string() } },
-  '/changerMotDePasse': { get: { output: z.string() } },
-  '/download/fichiers/:documentId': { params: { documentId: etapeDocumentIdValidator }, newDownload: true },
-  '/download/avisDocument/:etapeAvisId': { params: { etapeAvisId: etapeAvisIdValidator }, newDownload: true },
-  '/download/entrepriseDocuments/:documentId': { params: { documentId: entrepriseDocumentIdValidator }, newDownload: true },
-  '/download/activiteDocuments/:documentId': { params: { documentId: activiteDocumentIdValidator }, newDownload: true },
-  '/fichiers/:documentId': { params: { documentId: etapeDocumentIdValidator }, newDownload: true },
-  '/titres/:id': { params: { id: titreIdValidator }, download: true },
-  '/titres': { download: true },
-  '/titres_qgis': { download: true },
-  '/demarches': { download: true },
-  '/travaux': { download: true },
-  '/activites': { download: true },
-  '/utilisateurs': { download: true },
-  '/etape/zip/:etapeId': { params: { etapeId: etapeIdValidator }, download: true },
-  '/entreprises': { download: true },
+  '/deconnecter': { params: noParamsValidator, get: { output: z.string() } },
+  '/changerMotDePasse': { params: noParamsValidator, get: { output: z.string() } },
+  '/download/fichiers/:documentId': { params: z.object({ documentId: etapeDocumentIdValidator }), newDownload: true },
+  '/download/avisDocument/:etapeAvisId': { params: z.object({ etapeAvisId: etapeAvisIdValidator }), newDownload: true },
+  '/download/entrepriseDocuments/:documentId': { params: z.object({ documentId: entrepriseDocumentIdValidator }), newDownload: true },
+  '/download/activiteDocuments/:documentId': { params: z.object({ documentId: activiteDocumentIdValidator }), newDownload: true },
+  '/fichiers/:documentId': { params: z.object({ documentId: etapeDocumentIdValidator }), newDownload: true },
+  '/titres/:id': { params: z.object({ id: titreIdValidator }), download: true },
+  '/titres': { params: noParamsValidator, download: true },
+  '/titres_qgis': { params: noParamsValidator, download: true },
+  '/demarches': { params: noParamsValidator, download: true },
+  '/travaux': { params: noParamsValidator, download: true },
+  '/activites': { params: noParamsValidator, download: true },
+  '/utilisateurs': { params: noParamsValidator, download: true },
+  '/etape/zip/:etapeId': { params: etapeIdParamsValidator, download: true },
+  '/entreprises': { params: noParamsValidator, download: true },
 } as const satisfies { [k in CaminoRestRoute]: CaminoRoute<k> }
 
 const DOWNLOAD_FORMATS_IDS = ['xlsx', 'csv', 'ods', 'geojson', 'pdf', 'zip'] as const
@@ -242,7 +250,9 @@ const downloadFormatValidator = z.enum(DOWNLOAD_FORMATS_IDS)
 
 export type DownloadFormat = z.infer<typeof downloadFormatValidator>
 
-type ZodParseUrlParams<url> = url extends `${infer start}/${infer rest}` ? ZodParseUrlParams<start> & ZodParseUrlParams<rest> : url extends `:${infer param}` ? { [k in param]: ZodType } : {} // eslint-disable-line @typescript-eslint/ban-types
+type ZodObjectParsUrlParams<url> = ZodObject<ZodParseUrlParams<url>>
+
+type ZodParseUrlParams<url> = url extends `${infer start}/${infer rest}` ? ZodParseUrlParams<start> & ZodParseUrlParams<rest> : url extends `:${infer param}` ? { [k in param]: ZodTypeAny } : {} // eslint-disable-line @typescript-eslint/ban-types
 
 isTrue<Expect<ZodParseUrlParams<'/titre'>, {}>>
 isFalse<Expect<ZodParseUrlParams<'/titre'>, { id: ZodType }>>
@@ -250,13 +260,13 @@ isTrue<Expect<ZodParseUrlParams<'/titre/:id'>, { id: ZodType }>>
 isFalse<Expect<ZodParseUrlParams<'/titre/:id'>, {}>>
 isTrue<Expect<ZodParseUrlParams<'/titre/:titreId/:demarcheId'>, { titreId: ZodType; demarcheId: ZodType }>>
 
-type can<T, Method extends 'post' | 'get' | 'put' | 'delete' | 'download' | 'newDownload'> = T extends CaminoRestRoute
+type can<T, Method extends 'post' | 'newPost' | 'get' | 'put' | 'delete' | 'download' | 'newDownload'> = T extends CaminoRestRoute
   ? (typeof CaminoRestRoutes)[T] extends { [m in Method]: any }
     ? T
     : never
   : never
 
-type CaminoRestRouteList<Route, Method extends 'post' | 'get' | 'put' | 'delete' | 'download' | 'newDownload'> = Route extends readonly [infer First, ...infer Remaining]
+type CaminoRestRouteList<Route, Method extends 'post' | 'newPost' | 'get' | 'put' | 'delete' | 'download' | 'newDownload'> = Route extends readonly [infer First, ...infer Remaining]
   ? First extends can<First, Method>
     ? [First, ...CaminoRestRouteList<Remaining, Method>]
     : CaminoRestRouteList<Remaining, Method>
@@ -264,14 +274,13 @@ type CaminoRestRouteList<Route, Method extends 'post' | 'get' | 'put' | 'delete'
 
 export type GetRestRoutes = CaminoRestRouteList<typeof IDS, 'get'>[number]
 export type PostRestRoutes = CaminoRestRouteList<typeof IDS, 'post'>[number]
+export type NewPostRestRoutes = CaminoRestRouteList<typeof IDS, 'newPost'>[number]
 export type DeleteRestRoutes = CaminoRestRouteList<typeof IDS, 'delete'>[number]
 export type DownloadRestRoutes = CaminoRestRouteList<typeof IDS, 'download'>[number]
 export type NewDownloadRestRoutes = CaminoRestRouteList<typeof IDS, 'newDownload'>[number]
 export type PutRestRoutes = CaminoRestRouteList<typeof IDS, 'put'>[number]
 
-export type CaminoRestParams<Route extends CaminoRestRoute> = (typeof CaminoRestRoutes)[Route] extends { params: any }
-  ? { [k in keyof (typeof CaminoRestRoutes)[Route]['params']]: z.infer<(typeof CaminoRestRoutes)[Route]['params'][k]> }
-  : {}
+export type CaminoRestParams<Route extends CaminoRestRoute> = z.infer<(typeof CaminoRestRoutes)[Route]['params']>
 
 export const getRestRoute = <T extends CaminoRestRoute>(path: T, params: CaminoRestParams<T>, searchParams: Record<string, string | string[]> = {}) => {
   const urlParams = new URLSearchParams()
diff --git a/packages/common/src/roles.ts b/packages/common/src/roles.ts
index e33016e7493bfbc1b52465c81289476fd5717ef6..ed5e297ac2c1160ab3b7e56740d46b8c1750fafd 100644
--- a/packages/common/src/roles.ts
+++ b/packages/common/src/roles.ts
@@ -1,6 +1,7 @@
 import { administrationIdValidator } from './static/administrations.js'
 import { entrepriseValidator } from './entreprise.js'
 import { z } from 'zod'
+import { DeepReadonly } from './typescript-tools.js'
 
 export const ROLES = ['super', 'admin', 'editeur', 'lecteur', 'entreprise', 'bureau d’études', 'defaut'] as const
 
@@ -48,21 +49,22 @@ export type EntrepriseUserNotNull = z.infer<typeof entrepriseUserNotNullValidato
 const userNotNullValidator = z.union([superUserNotNullValidator, defautUserNotNullValidator, adminUserNotNullValidator, entrepriseUserNotNullValidator])
 export const userValidator = userNotNullValidator.nullable().optional()
 
-export const isSuper = (user: User): user is UserSuper => userPermissionCheck(user, 'super')
+export const isSuper = (user: DeepReadonly<User>): user is UserSuper => userPermissionCheck(user, 'super')
 
-export const isAdministration = (user: User): user is UserLecteur | UserAdmin | UserEditeur => isAdministrationAdmin(user) || isAdministrationEditeur(user) || isAdministrationLecteur(user)
-export const isAdministrationAdmin = (user: User): user is UserAdmin => userPermissionCheck(user, 'admin')
-export const isAdministrationEditeur = (user: User): user is UserEditeur => userPermissionCheck(user, 'editeur')
-export const isAdministrationLecteur = (user: User): user is UserLecteur => userPermissionCheck(user, 'lecteur')
+export const isAdministration = (user: DeepReadonly<User>): user is UserLecteur | UserAdmin | UserEditeur =>
+  isAdministrationAdmin(user) || isAdministrationEditeur(user) || isAdministrationLecteur(user)
+export const isAdministrationAdmin = (user: DeepReadonly<User>): user is UserAdmin => userPermissionCheck(user, 'admin')
+export const isAdministrationEditeur = (user: DeepReadonly<User>): user is UserEditeur => userPermissionCheck(user, 'editeur')
+export const isAdministrationLecteur = (user: DeepReadonly<User>): user is UserLecteur => userPermissionCheck(user, 'lecteur')
 export const isEntrepriseOrBureauDEtude = (user: User): user is UserEntreprise | UserBureaudEtudes => isEntreprise(user) || isBureauDEtudes(user)
 
 export const isEntreprise = (user: User): user is UserEntreprise => userPermissionCheck(user, 'entreprise')
 export const isBureauDEtudes = (user: User): user is UserBureaudEtudes => userPermissionCheck(user, 'bureau d’études')
-export const isDefault = (user: User): user is UserDefaut | undefined => !user || userPermissionCheck(user, 'defaut')
+export const isDefault = (user: DeepReadonly<User>): user is UserDefaut | undefined => !user || userPermissionCheck(user, 'defaut')
 
 export const isRole = (role: Role | string | undefined | null): role is Role => ROLES.includes(role)
 
-function userPermissionCheck(user: User, role: Role) {
+function userPermissionCheck(user: DeepReadonly<User>, role: Role) {
   return user?.role === role
 }
 
diff --git a/packages/common/src/static/documentsTypes.ts b/packages/common/src/static/documentsTypes.ts
index 96ec45c5c12c84a917fbcb80ae0e4282aa3f654d..c91b93d8cd89fcd7ddd388b78f113451e33cbb50 100644
--- a/packages/common/src/static/documentsTypes.ts
+++ b/packages/common/src/static/documentsTypes.ts
@@ -25,6 +25,7 @@ const FILE_UPLOAD_TYPE_IDS = ['pdf'] as const
 
 export const fileUploadTypeValidator = z.enum([...FILE_UPLOAD_TYPE_IDS, ...PERIMETRE_FILE_UPLOAD_TYPE_IDS])
 export const perimetreFileUploadTypeValidator = z.enum(PERIMETRE_FILE_UPLOAD_TYPE_IDS)
+export type PerimetreUploadType = z.infer<typeof perimetreFileUploadTypeValidator>
 export type FileUploadType = z.infer<typeof fileUploadTypeValidator>
 
 // prettier-ignore
diff --git a/packages/common/src/territoires.ts b/packages/common/src/territoires.ts
index 1de39c189c61a03cc6d0b670b8c69ba6f1356dea..f83bbb318468044c75266cce5bda1fe0681f3c97 100644
--- a/packages/common/src/territoires.ts
+++ b/packages/common/src/territoires.ts
@@ -2,12 +2,12 @@ import { CommuneId } from './static/communes.js'
 import { FacadeComputed, FacadesMaritimes, getDepartementsBySecteurs, getFacade, getFacadesComputed, SecteursMaritimes } from './static/facades.js'
 import { DepartementId, DepartementLabel, Departements, toDepartementId } from './static/departement.js'
 import { RegionId, RegionLabel, Regions } from './static/region.js'
-import { onlyUnique } from './typescript-tools.js'
+import { DeepReadonly, onlyUnique } from './typescript-tools.js'
 import { PaysId } from './static/pays.js'
 
 export const territoiresFind = (
   communesWithName: Record<CommuneId, string>,
-  communes?: { id: CommuneId; surface?: number | null }[] | null | undefined,
+  communes?: DeepReadonly<{ id: CommuneId; surface?: number | null }[]> | null | undefined,
   secteursMaritime?: SecteursMaritimes[] | null | undefined
 ): { communes: { nom: string; surface: null | number }[]; departements: DepartementLabel[]; regions: RegionLabel[]; facades: FacadeComputed[] } => {
   const result: {
@@ -56,7 +56,7 @@ export const territoiresFind = (
 }
 
 export const territoiresIdFind = (
-  communes: { id: CommuneId }[],
+  communes: DeepReadonly<{ id: CommuneId }[]>,
   secteursMaritime: SecteursMaritimes[]
 ): { departements: DepartementId[]; regions: RegionId[]; facades: FacadesMaritimes[]; pays: PaysId[] } => {
   const departements: DepartementId[] = [...getDepartementsBySecteurs(secteursMaritime), ...communes.map(({ id }) => toDepartementId(id))].filter(onlyUnique)
diff --git a/packages/common/src/zod-tools.ts b/packages/common/src/zod-tools.ts
index e57e7364b14c6757d113acd356390e8a7b273169..5e547e4ec1dfe6e704b4d633c31e912ac7e34bf7 100644
--- a/packages/common/src/zod-tools.ts
+++ b/packages/common/src/zod-tools.ts
@@ -25,3 +25,10 @@ export const makeFlattenValidator = <T extends z.ZodTypeAny>(schema: T) =>
       })
       .nullable(),
   })
+
+export type CaminoZodErrorReadableMessage = string & { __camino: 'ZodReadableMessage' }
+export type CaminoError<T extends string> = {
+  message: T
+  extra?: any
+  zodErrorReadableMessage?: CaminoZodErrorReadableMessage
+}
diff --git a/packages/ui/src/api/client-rest.ts b/packages/ui/src/api/client-rest.ts
index cb51d10cfe4b88081cac349b4ce5fc461de0bb58..854fb2ece7504d03af4225db0a91603dcb595602 100644
--- a/packages/ui/src/api/client-rest.ts
+++ b/packages/ui/src/api/client-rest.ts
@@ -10,8 +10,10 @@ import {
   GetRestRoutes,
   PostRestRoutes,
   PutRestRoutes,
+  NewPostRestRoutes,
 } from 'camino-common/src/rest'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
+import { CaminoError } from 'camino-common/src/zod-tools'
 import { DeepReadonly } from 'vue'
 import { z } from 'zod'
 
@@ -88,6 +90,30 @@ export const postWithJson = async <T extends PostRestRoutes>(
   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>,
+  body: z.infer<(typeof CaminoRestRoutes)[T]['newPost']['input']>
+): Promise<CaminoError<string> | z.infer<(typeof CaminoRestRoutes)[T]['newPost']['output']>> => {
+  const url = getUiRestRoute(path, params, {})
+
+  const defaultOptions = {
+    method: 'post',
+    headers: { 'Content-Type': 'application/json' },
+  }
+
+  const fetched = await fetch(url, { ...defaultOptions, body: JSON.stringify(body) })
+  if (fetched.ok) {
+    const bodyResponse = await fetched.json()
+    return bodyResponse
+  }
+  if (fetched.status === HTTP_STATUS.HTTP_STATUS_UNAUTHORIZED) {
+    window.location.replace('/oauth2/sign_in?rd=' + encodeURIComponent(window.location.href))
+  }
+  const bodyErrorResponse = await fetched.json()
+  return bodyErrorResponse
+}
+
 export const putWithJson = async <T extends PutRestRoutes>(
   path: T,
   params: CaminoRestParams<T>,
diff --git a/packages/ui/src/components/_common/dsfr-perimetre.stories.tsx b/packages/ui/src/components/_common/dsfr-perimetre.stories.tsx
index bd46f31e1e86e7b81803d8c27e0308f00c94f3fc..89a7179fa949a8e03b41affc5c7860d265656472 100644
--- a/packages/ui/src/components/_common/dsfr-perimetre.stories.tsx
+++ b/packages/ui/src/components/_common/dsfr-perimetre.stories.tsx
@@ -36,15 +36,6 @@ const geojson4326_perimetre: FeatureMultiPolygon = {
   },
 }
 
-const geojson4326_points: FeatureCollectionPoints = {
-  type: 'FeatureCollection',
-  features: [
-    { type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: [-52.5660583466962, 4.23944263425535] } },
-    { type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: [-52.5660583466962, 4.23944263425535] } },
-    { type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: [-52.5660583466962, 4.23944263425535] } },
-  ],
-}
-
 const geojson4326_forages: FeatureCollectionForages = {
   type: 'FeatureCollection',
   features: [
@@ -54,15 +45,9 @@ const geojson4326_forages: FeatureCollectionForages = {
 }
 
 const pushAction = action('push')
-const getGeojsonByGeoSystemeIdAction = action('getGeojsonByGeoSystemeId')
-const getTitresWithPerimetreForCarteAction = action('getGeojsonByGeoSystemeId')
-
-const apiClientMock: Pick<ApiClient, 'getTitresWithPerimetreForCarte' | 'getGeojsonByGeoSystemeId'> = {
-  getGeojsonByGeoSystemeId: (geojson, geoSystemeId) => {
-    getGeojsonByGeoSystemeIdAction(geojson, geoSystemeId)
+const getTitresWithPerimetreForCarteAction = action('getTitresWithPerimetreForCarte')
 
-    return Promise.resolve(geojson4326_points)
-  },
+const apiClientMock: Pick<ApiClient, 'getTitresWithPerimetreForCarte'> = {
   getTitresWithPerimetreForCarte: carte => {
     getTitresWithPerimetreForCarteAction(carte)
 
@@ -153,7 +138,6 @@ export const NoNeighborsNoSnapshot: StoryFn = () => (
         surface: km2Validator.parse(3),
       }}
       calculateNeighbours={false}
-      apiClient={apiClientMock}
       titreSlug={titreSlugValidator.parse('titre-slug')}
     />
   </>
@@ -433,7 +417,6 @@ export const CustomPoints: StoryFn = () => (
       }}
       initTab="points"
       calculateNeighbours={false}
-      apiClient={apiClientMock}
       titreSlug={titreSlugValidator.parse('titre-slug')}
     />
   </>
@@ -461,7 +444,6 @@ export const CustomPointsWithoutNameAndDesc: StoryFn = () => (
       }}
       initTab="points"
       calculateNeighbours={false}
-      apiClient={apiClientMock}
       titreSlug={titreSlugValidator.parse('titre-slug')}
     />
   </>
@@ -488,7 +470,6 @@ export const CustomPointsWithAnotherGeoSysteme: StoryFn = () => (
       }}
       initTab="points"
       calculateNeighbours={false}
-      apiClient={apiClientMock}
       titreSlug={titreSlugValidator.parse('titre-slug')}
     />
   </>
@@ -515,7 +496,6 @@ export const CustomPointsWithAnotherLegacyGeoSysteme: StoryFn = () => (
       }}
       initTab="points"
       calculateNeighbours={false}
-      apiClient={apiClientMock}
       titreSlug={titreSlugValidator.parse('titre-slug')}
     />
   </>
@@ -540,7 +520,6 @@ export const WithForages: StoryFn = () => (
     }}
     initTab="points"
     calculateNeighbours={false}
-    apiClient={apiClientMock}
     titreSlug={titreSlugValidator.parse('titre-slug')}
   />
 )
diff --git a/packages/ui/src/components/_common/dsfr-perimetre.tsx b/packages/ui/src/components/_common/dsfr-perimetre.tsx
index a2dc3c3b564584a25b9c4d95d56f0047271ed44b..b0441a8b6a4615d45f71dd5343b314030d50b910 100644
--- a/packages/ui/src/components/_common/dsfr-perimetre.tsx
+++ b/packages/ui/src/components/_common/dsfr-perimetre.tsx
@@ -28,10 +28,7 @@ type Props = {
   titreTypeId: TitreTypeId
   initTab?: TabId
   class?: HTMLAttributes['class']
-} & (
-  | { calculateNeighbours: true; router: Pick<CaminoRouter, 'push'>; apiClient: Pick<ApiClient, 'getTitresWithPerimetreForCarte' | 'getGeojsonByGeoSystemeId'> }
-  | { apiClient: Pick<ApiClient, 'getGeojsonByGeoSystemeId'>; calculateNeighbours: false }
-)
+} & ({ calculateNeighbours: true; router: Pick<CaminoRouter, 'push'>; apiClient: Pick<ApiClient, 'getTitresWithPerimetreForCarte'> } | { calculateNeighbours: false })
 
 const maxRows = 20
 
diff --git a/packages/ui/src/components/_ui/alert.stories.tsx b/packages/ui/src/components/_ui/alert.stories.tsx
index fc250106a9b20eff57e5b95ae794c1145b53940c..980220684e420a0eb79d0276ff49603494280a53 100644
--- a/packages/ui/src/components/_ui/alert.stories.tsx
+++ b/packages/ui/src/components/_ui/alert.stories.tsx
@@ -1,6 +1,7 @@
-import { Alert } from './alert'
+import { Alert, CaminoApiAlert } from './alert'
 import { Meta, StoryFn } from '@storybook/vue3'
 import { DsfrLink } from './dsfr-button'
+import { CaminoZodErrorReadableMessage } from 'camino-common/src/zod-tools'
 
 const meta: Meta = {
   title: 'Components/UI/Alert',
@@ -15,3 +16,13 @@ export const Info: StoryFn = () => <Alert type="info" title="Informations" descr
 export const InfoSansDescription: StoryFn = () => <Alert type="info" title="Informations" />
 export const InfoSmall: StoryFn = () => <Alert type="info" title="Informations" small={true} />
 export const InfoSmallLink: StoryFn = () => <Alert type="info" title={<DsfrLink icon={null} disabled={false} to={{ name: 'dashboard', params: {} }} title="le titre du lien" />} small={true} />
+
+export const ApiErrorSimple: StoryFn = () => <CaminoApiAlert caminoApiError={{ message: "Ceci est une alerte de l'api" }} />
+export const ApiErrorWithCompleteMessage: StoryFn = () => (
+  <CaminoApiAlert
+    caminoApiError={{
+      message: "Ceci est une alerte de l'api",
+      zodErrorReadableMessage: 'Validation error: Array must contain at least 3 element(s) at "features[0].geometry.coordinates[0][0]"' as CaminoZodErrorReadableMessage,
+    }}
+  />
+)
diff --git a/packages/ui/src/components/_ui/alert.stories_snapshots_ApiErrorSimple.html b/packages/ui/src/components/_ui/alert.stories_snapshots_ApiErrorSimple.html
new file mode 100644
index 0000000000000000000000000000000000000000..50a9fa951e6eaaa6621bb0a86174f7aff4411455
--- /dev/null
+++ b/packages/ui/src/components/_ui/alert.stories_snapshots_ApiErrorSimple.html
@@ -0,0 +1,3 @@
+<div class="fr-alert fr-alert--error fr-alert--sm">
+  <p>Ceci est une alerte de l'api</p>
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/_ui/alert.stories_snapshots_ApiErrorWithCompleteMessage.html b/packages/ui/src/components/_ui/alert.stories_snapshots_ApiErrorWithCompleteMessage.html
new file mode 100644
index 0000000000000000000000000000000000000000..49daa034289a1edf444b63d379ec99377637adbe
--- /dev/null
+++ b/packages/ui/src/components/_ui/alert.stories_snapshots_ApiErrorWithCompleteMessage.html
@@ -0,0 +1,3 @@
+<div class="fr-alert fr-alert--error">
+  <h3 class="fr-alert__title">Ceci est une alerte de l'api</h3>Validation error: Array must contain at least 3 element(s) at "features[0].geometry.coordinates[0][0]"
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/_ui/alert.tsx b/packages/ui/src/components/_ui/alert.tsx
index a2df5b4ac85d5dde261420206db4734755cf9a4d..77a8a5396c3d3e40a0223910749b27bfa7d57d5e 100644
--- a/packages/ui/src/components/_ui/alert.tsx
+++ b/packages/ui/src/components/_ui/alert.tsx
@@ -1,3 +1,4 @@
+import { CaminoError } from 'camino-common/src/zod-tools'
 import type { FunctionalComponent, HTMLAttributes } from 'vue'
 import type { JSX } from 'vue/jsx-runtime'
 type SmallProps = {
@@ -29,6 +30,14 @@ export const Alert: FunctionalComponent<Props> = props => {
   }
 }
 
+export const CaminoApiAlert: FunctionalComponent<{ caminoApiError: CaminoError<string>; class?: HTMLAttributes['class'] }> = props => {
+  const small = !('zodErrorReadableMessage' in props.caminoApiError)
+  if (small) {
+    return <Alert small={small} type="error" title={props.caminoApiError.message} />
+  }
+  return <Alert type="error" title={props.caminoApiError.message} description={props.caminoApiError.zodErrorReadableMessage} />
+}
+
 export const PageIntrouvableAlert: FunctionalComponent = () => {
   return <Alert type="error" title="Page Introuvable" small={true} />
 }
diff --git a/packages/ui/src/components/administration.stories.tsx b/packages/ui/src/components/administration.stories.tsx
index b29ec5ad51833e4116943a06b21658e1fdc751cc..9744b353816a40de148136633f8157d3215beb0f 100644
--- a/packages/ui/src/components/administration.stories.tsx
+++ b/packages/ui/src/components/administration.stories.tsx
@@ -45,12 +45,12 @@ export const Default: StoryFn = () => (
       administrationActiviteTypeEmailUpdate: () => {
         administrationActiviteTypeEmailUpdateAction()
 
-        return Promise.resolve(undefined)
+        return Promise.resolve(true)
       },
       administrationActiviteTypeEmailDelete: () => {
         administrationActiviteTypeEmailDeleteAction()
 
-        return Promise.resolve(undefined)
+        return Promise.resolve(true)
       },
     }}
   />
diff --git a/packages/ui/src/components/administration.tsx b/packages/ui/src/components/administration.tsx
index a8ea08cd8b8981d19e73db884a664fc2bb60ef0a..bf336b8176dd58a7a5f65c33ae7467d8032cb6d3 100644
--- a/packages/ui/src/components/administration.tsx
+++ b/packages/ui/src/components/administration.tsx
@@ -254,13 +254,13 @@ export const PureAdministration = defineComponent<Props>(props => {
                 user={props.user}
                 administrationId={props.administrationId}
                 activitesTypesEmails={item}
-                emailUpdate={(administrationId, administrationActiviteTypeEmail) => {
-                  props.apiClient.administrationActiviteTypeEmailUpdate(administrationId, administrationActiviteTypeEmail)
-                  loadActivitesTypesEmails()
+                emailUpdate={async (administrationId, administrationActiviteTypeEmail) => {
+                  await props.apiClient.administrationActiviteTypeEmailUpdate(administrationId, administrationActiviteTypeEmail)
+                  await loadActivitesTypesEmails()
                 }}
-                emailDelete={(administrationId, administrationActiviteTypeEmail) => {
-                  props.apiClient.administrationActiviteTypeEmailDelete(administrationId, administrationActiviteTypeEmail)
-                  loadActivitesTypesEmails()
+                emailDelete={async (administrationId, administrationActiviteTypeEmail) => {
+                  await props.apiClient.administrationActiviteTypeEmailDelete(administrationId, administrationActiviteTypeEmail)
+                  await loadActivitesTypesEmails()
                 }}
               />
             </>
diff --git a/packages/ui/src/components/administration/activites-types-emails.stories.tsx b/packages/ui/src/components/administration/activites-types-emails.stories.tsx
index 8bf1d80a1b781f2d3ef757c0aa4ea1c3b2271f8f..e9706404c6839a95313845b5984322a989fbf696 100644
--- a/packages/ui/src/components/administration/activites-types-emails.stories.tsx
+++ b/packages/ui/src/components/administration/activites-types-emails.stories.tsx
@@ -23,8 +23,14 @@ const activitesTypesEmails = [
   },
 ]
 
-const emailUpdate = action('emailUpdate')
-const emailDelete = action('emailDelete')
+const emailUpdate = async () => {
+  action('emailUpdate')
+  return Promise.resolve()
+}
+const emailDelete = async () => {
+  action('emailDelete')
+  return Promise.resolve()
+}
 export const EmailLectureVisible: StoryFn = () => (
   <ActivitesTypesEmails administrationId="aut-97300-01" user={null} activitesTypesEmails={activitesTypesEmails} emailUpdate={emailUpdate} emailDelete={emailDelete} />
 )
diff --git a/packages/ui/src/components/administration/activites-types-emails.tsx b/packages/ui/src/components/administration/activites-types-emails.tsx
index c70a94c58b7056cc3f9bf0a113a71ba8f67885f4..c794bd64b66c37f7a04635976e4655aac095ac64 100644
--- a/packages/ui/src/components/administration/activites-types-emails.tsx
+++ b/packages/ui/src/components/administration/activites-types-emails.tsx
@@ -16,8 +16,9 @@ interface Props {
   administrationId: AdministrationId
   user: User
   activitesTypesEmails: AdministrationActiviteTypeEmail[]
-  emailUpdate: (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => void
-  emailDelete: (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => void
+  // TODO 2024-06-26 tester et gérer les retours CaminoError
+  emailUpdate: (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => Promise<void>
+  emailDelete: (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => Promise<void>
 }
 const activiteTypeLabelize = (activiteType: { nom: string; id: string }) => {
   return `${capitalize(activiteType.nom)} (${activiteType.id.toUpperCase()})`
diff --git a/packages/ui/src/components/administration/administration-api-client.ts b/packages/ui/src/components/administration/administration-api-client.ts
index 2f75b1ee130202487ccaa898fe4beb29949e945b..f19e04be51e5bf139e748e071ee53c1dade90da3 100644
--- a/packages/ui/src/components/administration/administration-api-client.ts
+++ b/packages/ui/src/components/administration/administration-api-client.ts
@@ -1,13 +1,14 @@
 import { AdministrationId } from 'camino-common/src/static/administrations'
 import { AdminUserNotNull } from 'camino-common/src/roles'
 import { AdministrationActiviteTypeEmail } from 'camino-common/src/administrations'
-import { getWithJson, postWithJson } from '../../api/client-rest'
+import { getWithJson, newPostWithJson } from '../../api/client-rest'
+import { CaminoError } from 'camino-common/src/zod-tools'
 
 export interface AdministrationApiClient {
   administrationActivitesTypesEmails: (administrationId: AdministrationId) => Promise<AdministrationActiviteTypeEmail[]>
   administrationUtilisateurs: (administrationId: AdministrationId) => Promise<AdminUserNotNull[]>
-  administrationActiviteTypeEmailUpdate: (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => Promise<void>
-  administrationActiviteTypeEmailDelete: (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => Promise<void>
+  administrationActiviteTypeEmailUpdate: (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => Promise<CaminoError<string> | boolean>
+  administrationActiviteTypeEmailDelete: (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => Promise<CaminoError<string> | boolean>
 }
 
 export const administrationApiClient: AdministrationApiClient = {
@@ -17,10 +18,10 @@ export const administrationApiClient: AdministrationApiClient = {
     return getWithJson('/rest/administrations/:administrationId/utilisateurs', { administrationId })
   },
   administrationActiviteTypeEmailUpdate: async (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => {
-    await postWithJson('/rest/administrations/:administrationId/activiteTypeEmails', { administrationId }, administrationActiviteTypeEmail)
+    return newPostWithJson('/rest/administrations/:administrationId/activiteTypeEmails', { administrationId }, administrationActiviteTypeEmail)
   },
 
   administrationActiviteTypeEmailDelete: async (administrationId: AdministrationId, administrationActiviteTypeEmail: AdministrationActiviteTypeEmail) => {
-    await postWithJson('/rest/administrations/:administrationId/activiteTypeEmails/delete', { administrationId }, administrationActiviteTypeEmail)
+    return newPostWithJson('/rest/administrations/:administrationId/activiteTypeEmails/delete', { administrationId }, administrationActiviteTypeEmail)
   },
 }
diff --git a/packages/ui/src/components/demarche/demarche-etape.stories.tsx b/packages/ui/src/components/demarche/demarche-etape.stories.tsx
index fa8674dae3e4dc12d15a4e51fefec9ebcbb994fd..16e41954129b39ac7f61a0c5ab49129477cf296b 100644
--- a/packages/ui/src/components/demarche/demarche-etape.stories.tsx
+++ b/packages/ui/src/components/demarche/demarche-etape.stories.tsx
@@ -26,7 +26,6 @@ const titreSlug = titreSlugValidator.parse('titre-slug')
 const routerPushAction = action('routerPushAction')
 const deleteEtapeAction = action('deleteEtapeAction')
 const deposeEtapeAction = action('deposeEtapeAction')
-const getGeojsonByGeoSystemeIdAction = action('getGeojsonByGeoSystemeId')
 
 const entreprises: Entreprise[] = [
   { id: entrepriseIdValidator.parse('titulaire1'), nom: 'titulaire1', legal_siren: '' },
@@ -34,7 +33,7 @@ const entreprises: Entreprise[] = [
   { id: entrepriseIdValidator.parse('amodiataire1'), nom: 'amodiataire1', legal_siren: '' },
 ]
 
-const apiClient: Pick<ApiClient, 'deleteEtape' | 'deposeEtape' | 'getGeojsonByGeoSystemeId'> = {
+const apiClient: Pick<ApiClient, 'deleteEtape' | 'deposeEtape'> = {
   deleteEtape: etapeId => {
     deleteEtapeAction(etapeId)
 
@@ -45,12 +44,6 @@ const apiClient: Pick<ApiClient, 'deleteEtape' | 'deposeEtape' | 'getGeojsonByGe
 
     return Promise.resolve()
   },
-
-  getGeojsonByGeoSystemeId: (geojson, systemId) => {
-    getGeojsonByGeoSystemeIdAction(geojson, systemId)
-
-    return Promise.resolve(geojson)
-  },
 }
 
 const routerPushMock: Pick<CaminoRouter, 'push'> = {
diff --git a/packages/ui/src/components/demarche/demarche-etape.tsx b/packages/ui/src/components/demarche/demarche-etape.tsx
index 55bedd730568e440693f50872b16394817568a9f..5417f769d7d263ae0cf5fbd71432e7877dbda984 100644
--- a/packages/ui/src/components/demarche/demarche-etape.tsx
+++ b/packages/ui/src/components/demarche/demarche-etape.tsx
@@ -59,7 +59,7 @@ type Props = {
     nom: string
     titreStatutId: TitreStatutId
   }
-  apiClient: Pick<ApiClient, 'deleteEtape' | 'deposeEtape' | 'getGeojsonByGeoSystemeId'>
+  apiClient: Pick<ApiClient, 'deleteEtape' | 'deposeEtape'>
   router: Pick<CaminoRouter, 'push'>
   user: User
   entreprises: Entreprise[]
@@ -362,7 +362,6 @@ export const DemarcheEtape = defineComponent<Props>(props => {
           initTab={props.initTab}
           titreSlug={props.titre.slug}
           titreTypeId={props.titre.typeId}
-          apiClient={props.apiClient}
           calculateNeighbours={false}
           perimetre={{
             geojson4326_perimetre: props.etape.fondamentale.perimetre.geojson4326_perimetre,
diff --git a/packages/ui/src/components/etape-edition.stories.tsx b/packages/ui/src/components/etape-edition.stories.tsx
index e88a85b248b545d4ea0e5b13446da1311fd78e9e..09baa0b3d2b9a22369115d93736a6bb941be873a 100644
--- a/packages/ui/src/components/etape-edition.stories.tsx
+++ b/packages/ui/src/components/etape-edition.stories.tsx
@@ -29,7 +29,6 @@ const getPerimetreInfosByEtapeIdAction = action('getPerimetreInfosByEtapeId')
 const getEtapesTypesEtapesStatutsAction = action('getEtapesTypesEtapesStatuts')
 const geojsonImportAction = action('geojsonImport')
 const uploadTempDocumentAction = action('uploadTempDocument')
-const getGeojsonByGeoSystemeIdAction = action('getGeojsonByGeoSystemeId')
 const getEtapeDocumentsByEtapeIdAction = action('getEtapeDocumentsByEtapeId')
 const getEtapeEntrepriseDocumentsAction = action('getEtapeEntrepriseDocuments')
 const creerEntrepriseDocumentAction = action('creerEntrepriseDocument')
@@ -229,11 +228,6 @@ const apiClient: Props['apiClient'] = {
 
     return Promise.resolve(tempDocumentNameValidator.parse('name'))
   },
-  getGeojsonByGeoSystemeId(geojson, geoSystemeId) {
-    getGeojsonByGeoSystemeIdAction(geojson, geoSystemeId)
-
-    return Promise.resolve(geojson)
-  },
   getEtapeDocumentsByEtapeId(etapeId: EtapeId) {
     getEtapeDocumentsByEtapeIdAction(etapeId)
 
diff --git a/packages/ui/src/components/etape-edition.tsx b/packages/ui/src/components/etape-edition.tsx
index 533c98313b9744c5f58f5cec4bb8ad590bd3b533..af07629f87492cba6ea47ad1105db304ab32cc9a 100644
--- a/packages/ui/src/components/etape-edition.tsx
+++ b/packages/ui/src/components/etape-edition.tsx
@@ -63,7 +63,6 @@ export type Props = {
     | 'getEtapeHeritagePotentiel'
     | 'uploadTempDocument'
     | 'geojsonImport'
-    | 'getGeojsonByGeoSystemeId'
     | 'geojsonPointsImport'
     | 'geojsonForagesImport'
     | 'getEtapeDocumentsByEtapeId'
diff --git a/packages/ui/src/components/etape/etape-edit-form.stories.tsx b/packages/ui/src/components/etape/etape-edit-form.stories.tsx
index 55694efed0cec8dfa4c10d56fb354d120c18d52f..cff5aef3056b89344aa00c45a4a4753a14466896 100644
--- a/packages/ui/src/components/etape/etape-edit-form.stories.tsx
+++ b/packages/ui/src/components/etape/etape-edit-form.stories.tsx
@@ -112,7 +112,6 @@ const getEtapesTypesEtapesStatutsAction = action('getEtapesTypesEtapesStatuts')
 const getEtapeHeritagePotentielAction = action('getEtapeHeritagePotentiel')
 const geojsonImportAction = action('geojsonImport')
 const uploadTempDocumentAction = action('uploadTempDocumentAction')
-const getGeojsonByGeoSystemeIdAction = action('getGeojsonByGeoSystemeId')
 const getEtapeDocumentsByEtapeIdAction = action('getEtapeDocumentsByEtapeId')
 const getEtapeEntrepriseDocumentsAction = action('getEtapeEntrepriseDocuments')
 const creerEntrepriseDocumentAction = action('creerEntrepriseDocument')
@@ -209,11 +208,6 @@ const etapeEditFormApiClient: Props['apiClient'] = {
 
     return Promise.resolve(tempDocumentNameValidator.parse('name'))
   },
-  getGeojsonByGeoSystemeId(geojson, geoSystemeId) {
-    getGeojsonByGeoSystemeIdAction(geojson, geoSystemeId)
-
-    return Promise.resolve(geojson)
-  },
   getEtapeDocumentsByEtapeId(etapeId: EtapeId) {
     getEtapeDocumentsByEtapeIdAction(etapeId)
 
diff --git a/packages/ui/src/components/etape/etape-edit-form.tsx b/packages/ui/src/components/etape/etape-edit-form.tsx
index a2d9e48b0bdb4c5520279ec11474a3d85a9bbeab..b0c107b7b973a5502c1e2a562418d6668e78a6c3 100644
--- a/packages/ui/src/components/etape/etape-edit-form.tsx
+++ b/packages/ui/src/components/etape/etape-edit-form.tsx
@@ -77,7 +77,6 @@ export type Props = {
     | 'getEtapeHeritagePotentiel'
     | 'uploadTempDocument'
     | 'geojsonImport'
-    | 'getGeojsonByGeoSystemeId'
     | 'geojsonPointsImport'
     | 'geojsonForagesImport'
     | 'getEtapeDocumentsByEtapeId'
@@ -500,7 +499,7 @@ const EtapeEditFormInternal = defineComponent<
     etape: DeepReadonly<CoreEtapeCreationOrModification>
     documents: EtapeEditFormDocuments
     setEtape: (etape: DeepReadonly<CoreEtapeCreationOrModification>, documents: EtapeEditFormDocuments) => void
-    alertesUpdate: (alertes: Omit<PerimetreInformations, 'communes'>) => void
+    alertesUpdate: (alertes: Omit<DeepReadonly<PerimetreInformations>, 'communes'>) => void
   } & Omit<Props, 'etape'>
 >(props => {
   const documentsCompleteUpdate = (
diff --git a/packages/ui/src/components/etape/forages-import-popup.tsx b/packages/ui/src/components/etape/forages-import-popup.tsx
index 30a0b03fd50484037cbd82a76a176c474113284c..00f4cc5fc55ef279280d9e6873392304ece4e6ba 100644
--- a/packages/ui/src/components/etape/forages-import-popup.tsx
+++ b/packages/ui/src/components/etape/forages-import-popup.tsx
@@ -8,14 +8,14 @@ import { Alert } from '../_ui/alert'
 import { GeoSystemeTypeahead } from '../_common/geosysteme-typeahead'
 import { DsfrInputRadio } from '../_ui/dsfr-input-radio'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
-
-type FileType = 'geojson' | 'shp' | 'csv'
+import { CaminoError } from 'camino-common/src/zod-tools'
+import { PerimetreUploadType } from 'camino-common/src/static/documentsTypes'
 
 interface Props {
   apiClient: Pick<ApiClient, 'uploadTempDocument' | 'geojsonForagesImport'>
   geoSystemeId: GeoSystemeId
-  initialSelectedFileType?: FileType
-  result: (value: { geojson4326: FeatureCollectionForages; origin: FeatureCollectionForages } | Error) => void
+  initialSelectedFileType?: PerimetreUploadType
+  result: (value: { geojson4326: FeatureCollectionForages; origin: FeatureCollectionForages } | CaminoError<string>) => void
   close: () => void
 }
 
@@ -26,8 +26,8 @@ export const ForagesImportPopup = defineComponent<Props>(props => {
     importFile.value = file
   }
 
-  const fileType = ref<FileType | null>(props.initialSelectedFileType ?? null)
-  const fileTypeSelected = (value: FileType) => {
+  const fileType = ref<PerimetreUploadType | null>(props.initialSelectedFileType ?? null)
+  const fileTypeSelected = (value: PerimetreUploadType) => {
     fileType.value = value
   }
 
diff --git a/packages/ui/src/components/etape/perimetre-edit.stories.tsx b/packages/ui/src/components/etape/perimetre-edit.stories.tsx
index cfdb02a1d27483b45e765282131e04bc3ccc5afe..660c5d61fcf0025d0150194c9c14a82a12fa4630 100644
--- a/packages/ui/src/components/etape/perimetre-edit.stories.tsx
+++ b/packages/ui/src/components/etape/perimetre-edit.stories.tsx
@@ -19,7 +19,6 @@ export default meta
 
 const geojsonImportAction = action('geojsonImport')
 const uploadTempDocumentAction = action('uploadTempDocumentAction')
-const getGeojsonByGeoSystemeIdAction = action('getGeojsonByGeoSystemeId')
 const onEtapeChangeAction = action('onEtapeChange')
 const onPointsChangeAction = action('onPointsChange')
 const onForagesChangeAction = action('onForagesChange')
@@ -44,7 +43,7 @@ const perimetre: FeatureMultiPolygon = {
   },
 }
 
-const apiClient: Pick<ApiClient, 'uploadTempDocument' | 'geojsonImport' | 'getGeojsonByGeoSystemeId' | 'geojsonPointsImport' | 'geojsonForagesImport'> = {
+const apiClient: Pick<ApiClient, 'uploadTempDocument' | 'geojsonImport' | 'geojsonPointsImport' | 'geojsonForagesImport'> = {
   geojsonImport(body, geoSystemeId) {
     geojsonImportAction(body, geoSystemeId)
 
@@ -65,11 +64,6 @@ const apiClient: Pick<ApiClient, 'uploadTempDocument' | 'geojsonImport' | 'getGe
 
     return Promise.resolve(tempDocumentNameValidator.parse('name'))
   },
-  getGeojsonByGeoSystemeId(geojson, geoSystemeId) {
-    getGeojsonByGeoSystemeIdAction(geojson, geoSystemeId)
-
-    return Promise.resolve(geojson)
-  },
 }
 
 const onEtapeChange = (geojsonInformations: GeojsonInformations) => {
diff --git a/packages/ui/src/components/etape/perimetre-edit.tsx b/packages/ui/src/components/etape/perimetre-edit.tsx
index 5075d85d961a49be34b2adc6fa3812cc61ae4166..83c61b3a08cde15ad31ad15664bc3365d8787c26 100644
--- a/packages/ui/src/components/etape/perimetre-edit.tsx
+++ b/packages/ui/src/components/etape/perimetre-edit.tsx
@@ -7,15 +7,16 @@ import { FeatureCollectionForages, FeatureCollectionPoints, GeojsonInformations
 import { TitreTypeId } from 'camino-common/src/static/titresTypes'
 import { DsfrPerimetre } from '../_common/dsfr-perimetre'
 import { TitreSlug } from 'camino-common/src/validators/titres'
-import { Alert } from '../_ui/alert'
+import { CaminoApiAlert } from '../_ui/alert'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 import { PointsImportPopup } from './points-import-popup'
 import { ForagesImportPopup } from './forages-import-popup'
 import { canHaveForages } from 'camino-common/src/permissions/titres'
 import { CoreEtapeCreationOrModification } from './etape-api-client'
+import { CaminoError } from 'camino-common/src/zod-tools'
 
 export interface Props {
-  apiClient: Pick<ApiClient, 'uploadTempDocument' | 'geojsonImport' | 'getGeojsonByGeoSystemeId' | 'geojsonPointsImport' | 'geojsonForagesImport'>
+  apiClient: Pick<ApiClient, 'uploadTempDocument' | 'geojsonImport' | 'geojsonPointsImport' | 'geojsonForagesImport'>
   etape: DeepReadonly<Pick<CoreEtapeCreationOrModification, 'perimetre'>>
   titreTypeId: TitreTypeId
   titreSlug: TitreSlug
@@ -27,7 +28,6 @@ export interface Props {
 }
 
 type DisplayPerimetreProps = {
-  apiClient: Pick<ApiClient, 'getGeojsonByGeoSystemeId'>
   perimetre: Props['etape']['perimetre'] | null
   titreSlug: TitreSlug
   titreTypeId: TitreTypeId
@@ -45,7 +45,6 @@ const DisplayPerimetre: FunctionalComponent<DisplayPerimetreProps> = props => {
       <div>
         <DsfrPerimetre
           calculateNeighbours={false}
-          apiClient={props.apiClient}
           perimetre={{
             geojson4326_points: props.perimetre?.value?.geojson4326Points ?? null,
             geojson4326_perimetre: props.perimetre.value.geojson4326Perimetre,
@@ -70,7 +69,7 @@ export const PerimetreEdit = defineComponent<Props>(props => {
   const importPerimetrePopup = ref<boolean>(false)
   const importPointsPopup = ref<boolean>(false)
   const importForagesPopup = ref<boolean>(false)
-  const importError = ref<boolean>(false)
+  const importError = ref<CaminoError<string> | null>(null)
 
   const updateHeritage = (heritage: Props['etape']['perimetre']) => {
     props.onHeritageChange(heritage)
@@ -98,32 +97,32 @@ export const PerimetreEdit = defineComponent<Props>(props => {
     importForagesPopup.value = false
   }
 
-  const result = (value: GeojsonInformations | Error) => {
+  const result = (value: GeojsonInformations | CaminoError<string>) => {
     if ('geojson4326_perimetre' in value) {
-      importError.value = false
+      importError.value = null
       props.onEtapeChange(value)
     } else {
-      importError.value = true
+      importError.value = value
       console.error(value)
     }
   }
 
-  const resultPoints = (value: { geojson4326: FeatureCollectionPoints; origin: FeatureCollectionPoints } | Error) => {
+  const resultPoints = (value: { geojson4326: FeatureCollectionPoints; origin: FeatureCollectionPoints } | CaminoError<string>) => {
     if ('geojson4326' in value) {
-      importError.value = false
+      importError.value = null
       props.onPointsChange(value.geojson4326, value.origin)
     } else {
-      importError.value = true
+      importError.value = value
       console.error(value)
     }
   }
 
-  const resultForages = (value: { geojson4326: FeatureCollectionForages; origin: FeatureCollectionForages } | Error) => {
+  const resultForages = (value: { geojson4326: FeatureCollectionForages; origin: FeatureCollectionForages } | CaminoError<string>) => {
     if ('geojson4326' in value) {
-      importError.value = false
+      importError.value = null
       props.onForagesChange(value.geojson4326, value.origin)
     } else {
-      importError.value = true
+      importError.value = value
       console.error(value)
     }
   }
@@ -145,14 +144,13 @@ export const PerimetreEdit = defineComponent<Props>(props => {
               </>
             ) : null}
 
-            {importError.value ? <Alert class="fr-mt-2w" title="Une erreur est survenue lors de l’import de votre fichier." type="error" description="Vérifiez le contenu de votre fichier" /> : null}
+            {isNotNullNorUndefined(importError.value) ? <CaminoApiAlert class="fr-mt-2w" caminoApiError={importError.value} /> : null}
 
-            <DisplayPerimetre class="fr-mt-2w" apiClient={props.apiClient} perimetre={props.etape.perimetre} titreSlug={props.titreSlug} initTab={props.initTab} titreTypeId={props.titreTypeId} />
+            <DisplayPerimetre class="fr-mt-2w" perimetre={props.etape.perimetre} titreSlug={props.titreSlug} initTab={props.initTab} titreTypeId={props.titreTypeId} />
           </div>
         )}
         read={heritage => (
           <DisplayPerimetre
-            apiClient={props.apiClient}
             perimetre={isNotNullNorUndefined(heritage) ? { ...props.etape.perimetre, value: heritage.value } : null}
             titreSlug={props.titreSlug}
             titreTypeId={props.titreTypeId}
diff --git a/packages/ui/src/components/etape/perimetre-import-popup.tsx b/packages/ui/src/components/etape/perimetre-import-popup.tsx
index 92f0931e942f82158197f8b9984145be98ecd331..b66147913f2a31a9af76ef6bd9c34bb19dffc8fd 100644
--- a/packages/ui/src/components/etape/perimetre-import-popup.tsx
+++ b/packages/ui/src/components/etape/perimetre-import-popup.tsx
@@ -10,17 +10,17 @@ import { GeoSystemeTypeahead } from '../_common/geosysteme-typeahead'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 import { Alert } from '../_ui/alert'
 import { DsfrInputRadio } from '../_ui/dsfr-input-radio'
+import { CaminoError } from 'camino-common/src/zod-tools'
+import { PerimetreUploadType } from 'camino-common/src/static/documentsTypes'
 
 interface Props {
   apiClient: Pick<ApiClient, 'uploadTempDocument' | 'geojsonImport'>
   titreTypeId: TitreTypeId
   titreSlug: TitreSlug
-  result: (param: GeojsonInformations | Error) => void
+  result: (param: GeojsonInformations | CaminoError<string>) => void
   close: () => void
 }
 
-type FileType = 'geojson' | 'shp' | 'csv'
-
 const defaultGeoSystemeId = GeoSystemes[4326].id
 export const PerimetreImportPopup = defineComponent<Props>(props => {
   const systemeGeographique = ref<GeoSystemeId>(defaultGeoSystemeId)
@@ -39,9 +39,9 @@ export const PerimetreImportPopup = defineComponent<Props>(props => {
     }
   }
 
-  const fileType = ref<FileType | null>(null)
+  const fileType = ref<PerimetreUploadType | null>(null)
 
-  const fileTypeSelected = (value: FileType) => {
+  const fileTypeSelected = (value: PerimetreUploadType) => {
     fileType.value = value
   }
 
diff --git a/packages/ui/src/components/etape/points-import-popup.tsx b/packages/ui/src/components/etape/points-import-popup.tsx
index b96d696349ddbfafc278b87e118da24ad1c5aa9e..6379e6c0da4b72ea982f98e12fceb4150d048ed1 100644
--- a/packages/ui/src/components/etape/points-import-popup.tsx
+++ b/packages/ui/src/components/etape/points-import-popup.tsx
@@ -6,11 +6,12 @@ import { ApiClient } from '@/api/api-client'
 import { FeatureCollectionPoints } from 'camino-common/src/perimetre'
 import { Alert } from '../_ui/alert'
 import { GeoSystemeTypeahead } from '../_common/geosysteme-typeahead'
+import { CaminoError } from 'camino-common/src/zod-tools'
 
 interface Props {
   apiClient: Pick<ApiClient, 'uploadTempDocument' | 'geojsonPointsImport'>
   geoSystemeId: GeoSystemeId
-  result: (value: { geojson4326: FeatureCollectionPoints; origin: FeatureCollectionPoints } | Error) => void
+  result: (value: { geojson4326: FeatureCollectionPoints; origin: FeatureCollectionPoints } | CaminoError<string>) => void
   close: () => void
 }
 
diff --git a/packages/ui/src/components/titre.stories.tsx b/packages/ui/src/components/titre.stories.tsx
index fc837282c46611eaa0caa77aa724936d82727811..0d8fa4b3bb996437dce96eb9ff1ea5852282b9d2 100644
--- a/packages/ui/src/components/titre.stories.tsx
+++ b/packages/ui/src/components/titre.stories.tsx
@@ -39,7 +39,6 @@ const loadLinkableTitresAction = action('loadLinkableTitresAction')
 const getTitreUtilisateurAbonneAction = action('getTitreUtilisateurAbonneAction')
 const titreUtilisateurAbonneAction = action('titreUtilisateurAbonneAction')
 const linkTitresAction = action('linkTitresAction')
-const getGeojsonByGeoSystemeIdAction = action('getGeojsonByGeoSystemeId')
 const date = toCaminoDate('2023-10-24')
 
 type PropsApiClient = Pick<
@@ -58,7 +57,6 @@ type PropsApiClient = Pick<
   | 'createDemarche'
   | 'updateDemarche'
   | 'deleteDemarche'
-  | 'getGeojsonByGeoSystemeId'
 >
 const routerPushMock: Pick<CaminoRouter, 'push' | 'replace'> = {
   push: to => {
@@ -337,11 +335,6 @@ const apiClient: PropsApiClient = {
 
     return Promise.resolve()
   },
-  getGeojsonByGeoSystemeId: (geojson, systemId) => {
-    getGeojsonByGeoSystemeIdAction(geojson, systemId)
-
-    return Promise.resolve(geojson)
-  },
 }
 
 const currentDate = caminoDateValidator.parse('2024-01-09')
diff --git a/packages/ui/src/components/titre.tsx b/packages/ui/src/components/titre.tsx
index 49499a22fd26a00b598cbf9eeddc8e950ab03f7c..ea8040f0c97468465eafca97929c75cea9bbf32a 100644
--- a/packages/ui/src/components/titre.tsx
+++ b/packages/ui/src/components/titre.tsx
@@ -114,7 +114,6 @@ interface Props {
     | 'createDemarche'
     | 'updateDemarche'
     | 'deleteDemarche'
-    | 'getGeojsonByGeoSystemeId'
   >
   router: Pick<CaminoRouter, 'push' | 'replace'>
   initTab?: TabId
diff --git a/packages/ui/src/components/titre/perimetre-api-client.ts b/packages/ui/src/components/titre/perimetre-api-client.ts
index c5f6f7ffcc19c2053696f414f65de307314024a8..12f0eaa38cb1ae641ec38cb6934f2dc4fc5f3d63 100644
--- a/packages/ui/src/components/titre/perimetre-api-client.ts
+++ b/packages/ui/src/components/titre/perimetre-api-client.ts
@@ -1,5 +1,4 @@
 import {
-  FeatureCollectionPoints,
   GeojsonImportBody,
   GeojsonImportForagesBody,
   GeojsonImportForagesResponse,
@@ -9,31 +8,28 @@ import {
   PerimetreInformations,
 } from 'camino-common/src/perimetre'
 import { GeoSystemeId } from 'camino-common/src/static/geoSystemes'
-import { getWithJson, postWithJson } from '../../api/client-rest'
+import { getWithJson, newPostWithJson } from '../../api/client-rest'
 import { EtapeIdOrSlug } from 'camino-common/src/etape'
 import { DemarcheIdOrSlug } from 'camino-common/src/demarche'
+import { CaminoError } from 'camino-common/src/zod-tools'
 
 export interface PerimetreApiClient {
-  getGeojsonByGeoSystemeId: (geojson: FeatureCollectionPoints, geoSystemeId: GeoSystemeId) => Promise<FeatureCollectionPoints>
-  geojsonImport: (body: GeojsonImportBody, geoSystemeId: GeoSystemeId) => Promise<GeojsonInformations | Error>
-  geojsonPointsImport: (body: GeojsonImportPointsBody, geoSystemeId: GeoSystemeId) => Promise<GeojsonImportPointsResponse | Error>
-  geojsonForagesImport: (body: GeojsonImportForagesBody, geoSystemeId: GeoSystemeId) => Promise<GeojsonImportForagesResponse | Error>
+  geojsonImport: (body: GeojsonImportBody, geoSystemeId: GeoSystemeId) => Promise<CaminoError<string> | GeojsonInformations>
+  geojsonPointsImport: (body: GeojsonImportPointsBody, geoSystemeId: GeoSystemeId) => Promise<GeojsonImportPointsResponse | CaminoError<string>>
+  geojsonForagesImport: (body: GeojsonImportForagesBody, geoSystemeId: GeoSystemeId) => Promise<GeojsonImportForagesResponse | CaminoError<string>>
   getPerimetreInfosByEtapeId: (etapeId: EtapeIdOrSlug) => Promise<PerimetreInformations>
   getPerimetreInfosByDemarcheId: (demarcheId: DemarcheIdOrSlug) => Promise<PerimetreInformations>
 }
 
 export const perimetreApiClient: PerimetreApiClient = {
-  getGeojsonByGeoSystemeId: (geojson: FeatureCollectionPoints, geoSystemeId: GeoSystemeId): Promise<FeatureCollectionPoints> => {
-    return postWithJson('/rest/geojson_points/:geoSystemeId', { geoSystemeId }, geojson)
-  },
   geojsonImport: (body: GeojsonImportBody, geoSystemeId: GeoSystemeId) => {
-    return postWithJson('/rest/geojson/import/:geoSystemeId', { geoSystemeId }, body)
+    return newPostWithJson('/rest/geojson/import/:geoSystemeId', { geoSystemeId }, body)
   },
   geojsonPointsImport: (body: GeojsonImportPointsBody, geoSystemeId: GeoSystemeId) => {
-    return postWithJson('/rest/geojson_points/import/:geoSystemeId', { geoSystemeId }, body)
+    return newPostWithJson('/rest/geojson_points/import/:geoSystemeId', { geoSystemeId }, body)
   },
   geojsonForagesImport: (body: GeojsonImportForagesBody, geoSystemeId: GeoSystemeId) => {
-    return postWithJson('/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId }, body)
+    return newPostWithJson('/rest/geojson_forages/import/:geoSystemeId', { geoSystemeId }, body)
   },
   getPerimetreInfosByEtapeId: (etapeId: EtapeIdOrSlug) => {
     return getWithJson('/rest/etapes/:etapeId/geojson', { etapeId })
diff --git a/packages/ui/src/components/titre/titre-demarche.tsx b/packages/ui/src/components/titre/titre-demarche.tsx
index 27ec9f4d23b2c34ae72384372668c6d08b854d87..efcdb1825aca4a7f0728c38f9d80b97c277ca005 100644
--- a/packages/ui/src/components/titre/titre-demarche.tsx
+++ b/packages/ui/src/components/titre/titre-demarche.tsx
@@ -29,7 +29,7 @@ type Props = {
   titre: Pick<TitreGet, 'id' | 'slug' | 'titre_type_id' | 'titre_statut_id' | 'nom'>
   demarches: TitreGetDemarche[]
   currentDemarcheSlug: DemarcheSlug
-  apiClient: Pick<ApiClient, 'deleteEtape' | 'deposeEtape' | 'getTitresWithPerimetreForCarte' | 'createDemarche' | 'updateDemarche' | 'deleteDemarche' | 'getGeojsonByGeoSystemeId'>
+  apiClient: Pick<ApiClient, 'deleteEtape' | 'deposeEtape' | 'getTitresWithPerimetreForCarte' | 'createDemarche' | 'updateDemarche' | 'deleteDemarche'>
   demarcheCreatedOrUpdated: (demarcheSlug: DemarcheSlug) => Promise<void>
   demarcheDeleted: () => Promise<void>
   router: Pick<CaminoRouter, 'push'>
diff --git a/packages/ui/src/components/utilisateur.stories.tsx b/packages/ui/src/components/utilisateur.stories.tsx
index 17a402e149a5544d817484bf3ddaf339764df73a..61e501223ae10396f7f872647c73dd9645ba2193 100644
--- a/packages/ui/src/components/utilisateur.stories.tsx
+++ b/packages/ui/src/components/utilisateur.stories.tsx
@@ -3,7 +3,7 @@ import { Meta, StoryFn } from '@storybook/vue3'
 import { newEntrepriseId } from 'camino-common/src/entreprise'
 import { testBlankUser } from 'camino-common/src/tests-utils'
 import { PureUtilisateur, Props } from './utilisateur'
-import { toUtilisateurId } from 'camino-common/src/roles'
+import { toUtilisateurId, utilisateurIdValidator } from 'camino-common/src/roles'
 
 const meta: Meta<typeof PureUtilisateur> = {
   title: 'Components/Utilisateur',
@@ -50,7 +50,7 @@ export const MySelf: StoryFn = () => (
   <PureUtilisateur
     entreprises={[{ id: newEntrepriseId('id'), nom: 'Entreprise1', legal_siren: null }]}
     user={{ ...testBlankUser, id: toUtilisateurId('id'), role: 'super' }}
-    utilisateurId="id"
+    utilisateurId={utilisateurIdValidator.parse('id')}
     passwordUpdate={passwordUpdate}
     apiClient={apiClientMock}
   />
@@ -60,7 +60,7 @@ export const Loading: StoryFn = () => (
   <PureUtilisateur
     entreprises={[{ id: newEntrepriseId('id'), nom: 'Entreprise1', legal_siren: null }]}
     user={{ ...testBlankUser, id: toUtilisateurId('id'), role: 'super' }}
-    utilisateurId="id"
+    utilisateurId={utilisateurIdValidator.parse('id')}
     passwordUpdate={passwordUpdate}
     apiClient={{
       ...apiClientMock,
@@ -73,7 +73,7 @@ export const error: StoryFn = () => (
   <PureUtilisateur
     entreprises={[{ id: newEntrepriseId('id'), nom: 'Entreprise1', legal_siren: null }]}
     user={{ ...testBlankUser, id: toUtilisateurId('anotherId'), role: 'super' }}
-    utilisateurId="id"
+    utilisateurId={utilisateurIdValidator.parse('id')}
     passwordUpdate={passwordUpdate}
     apiClient={{
       ...apiClientMock,
@@ -87,7 +87,7 @@ export const AnotherUser: StoryFn = () => (
   <PureUtilisateur
     entreprises={[{ id: newEntrepriseId('id'), nom: 'Entreprise1', legal_siren: null }]}
     user={{ ...testBlankUser, id: toUtilisateurId('anotherId'), role: 'super' }}
-    utilisateurId="id"
+    utilisateurId={utilisateurIdValidator.parse('id')}
     passwordUpdate={passwordUpdate}
     apiClient={apiClientMock}
   />
diff --git a/packages/ui/src/components/utilisateur.tsx b/packages/ui/src/components/utilisateur.tsx
index ada9d8b720001aed06fcfa871982a2935e544ca3..f1edc5c95d1fb71173f1dbf17ce57ada0c61281c 100644
--- a/packages/ui/src/components/utilisateur.tsx
+++ b/packages/ui/src/components/utilisateur.tsx
@@ -1,6 +1,6 @@
 import { computed, defineComponent, inject, onMounted, ref, watch } from 'vue'
 import { Card } from './_ui/card'
-import { User } from 'camino-common/src/roles'
+import { User, UtilisateurId, utilisateurIdValidator } from 'camino-common/src/roles'
 import { QGisToken } from './utilisateur/qgis-token'
 import { AsyncData } from '@/api/client-rest'
 import { useRoute, useRouter } from 'vue-router'
@@ -16,6 +16,7 @@ import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 import { entreprisesKey, userKey } from '@/moi'
 import { DsfrInputCheckbox } from './_ui/dsfr-input-checkbox'
 import { DsfrButtonIcon } from './_ui/dsfr-button'
+import { Alert } from './_ui/alert'
 
 export const Utilisateur = defineComponent({
   setup() {
@@ -25,7 +26,7 @@ export const Utilisateur = defineComponent({
     const user = inject(userKey)
     const entreprises = inject(entreprisesKey, ref([]))
 
-    const deleteUtilisateur = async (userId: string) => {
+    const deleteUtilisateur = async (userId: UtilisateurId) => {
       const isMe: boolean = (user && userId === user.id) ?? false
       if (isMe) {
         // TODO 2023-10-23 type window.location pour s'appuyer sur nos routes rest et pas sur n'importe quoi
@@ -41,28 +42,37 @@ export const Utilisateur = defineComponent({
     const passwordUpdate = () => {
       window.location.replace('/apiUrl/changerMotDePasse')
     }
-    const utilisateurId = ref<string>(Array.isArray(route.params.id) ? route.params.id[0] : route.params.id)
-    watch(
-      () => route.params.id,
-      newId => {
-        utilisateurId.value = Array.isArray(newId) ? newId[0] : newId
+    const utilisateurId = computed<UtilisateurId | null>(() => {
+      const idOrSlug = route.params.id
+      const validated = utilisateurIdValidator.safeParse(idOrSlug)
+
+      if (validated.success) {
+        return validated.data
       }
-    )
+
+      return null
+    })
 
     return () => (
-      <PureUtilisateur
-        passwordUpdate={passwordUpdate}
-        apiClient={{ ...utilisateurApiClient, updateUtilisateur, removeUtilisateur: deleteUtilisateur }}
-        utilisateurId={utilisateurId.value}
-        user={user}
-        entreprises={entreprises.value}
-      />
+      <>
+        {utilisateurId.value ? (
+          <PureUtilisateur
+            passwordUpdate={passwordUpdate}
+            apiClient={{ ...utilisateurApiClient, updateUtilisateur, removeUtilisateur: deleteUtilisateur }}
+            utilisateurId={utilisateurId.value}
+            user={user}
+            entreprises={entreprises.value}
+          />
+        ) : (
+          <Alert title="Utilisateur inconnu" type="error" small={true} />
+        )}
+      </>
     )
   },
 })
 export interface Props {
   user: User
-  utilisateurId: string
+  utilisateurId: UtilisateurId
   apiClient: Pick<UtilisateurApiClient, 'getQGISToken' | 'getUtilisateur' | 'removeUtilisateur' | 'getUtilisateurNewsletter' | 'updateUtilisateur' | 'updateUtilisateurNewsletter'>
   entreprises: Entreprise[]
   passwordUpdate: () => void
@@ -121,7 +131,7 @@ export const PureUtilisateur = defineComponent<Props>(props => {
     await get()
   }
 
-  const updateSubscription = async (utilisateurId: string, newsletterChecked: boolean) => {
+  const updateSubscription = async (utilisateurId: UtilisateurId, newsletterChecked: boolean) => {
     subscription.value = { status: 'LOADING' }
     try {
       await props.apiClient.updateUtilisateurNewsletter(utilisateurId, newsletterChecked)
diff --git a/packages/ui/src/components/utilisateur/utilisateur-api-client.ts b/packages/ui/src/components/utilisateur/utilisateur-api-client.ts
index bba89f2b58eff74955cf5e69279dc32fde3611f2..0924cae7e3a9c77bb45ae4f9378d61b126db22cf 100644
--- a/packages/ui/src/components/utilisateur/utilisateur-api-client.ts
+++ b/packages/ui/src/components/utilisateur/utilisateur-api-client.ts
@@ -4,15 +4,15 @@ import { QGISToken, UtilisateurToEdit } from 'camino-common/src/utilisateur'
 
 import gql from 'graphql-tag'
 import { getWithJson, postWithJson } from '../../api/client-rest'
-import { Role } from 'camino-common/src/roles'
+import { Role, UtilisateurId } from 'camino-common/src/roles'
 import { AdministrationId } from 'camino-common/src/static/administrations'
 
 export interface UtilisateurApiClient {
-  getUtilisateur: (userId: string) => Promise<Utilisateur>
-  getUtilisateurNewsletter: (userId: string) => Promise<boolean>
-  updateUtilisateurNewsletter: (userId: string, subscribe: boolean) => Promise<void>
+  getUtilisateur: (userId: UtilisateurId) => Promise<Utilisateur>
+  getUtilisateurNewsletter: (userId: UtilisateurId) => Promise<boolean>
+  updateUtilisateurNewsletter: (userId: UtilisateurId, subscribe: boolean) => Promise<void>
   newsletterInscrire: (email: string) => Promise<void>
-  removeUtilisateur: (userId: string) => Promise<void>
+  removeUtilisateur: (userId: UtilisateurId) => Promise<void>
   updateUtilisateur: (user: UtilisateurToEdit) => Promise<void>
   getQGISToken: () => Promise<QGISToken>
   getUtilisateurs: (params: UtilisateursParams) => Promise<{ elements: Utilisateur[]; total: number }>
@@ -96,8 +96,8 @@ export const utilisateurApiClient: UtilisateurApiClient = {
 
     return data
   },
-  getUtilisateurNewsletter: async (userId: string) => getWithJson('/rest/utilisateurs/:id/newsletter', { id: userId }),
-  updateUtilisateurNewsletter: async (userId: string, newsletter: boolean) => {
+  getUtilisateurNewsletter: async (userId: UtilisateurId) => getWithJson('/rest/utilisateurs/:id/newsletter', { id: userId }),
+  updateUtilisateurNewsletter: async (userId: UtilisateurId, newsletter: boolean) => {
     await postWithJson('/rest/utilisateurs/:id/newsletter', { id: userId }, { newsletter })
   },
   newsletterInscrire: async (email: string) => {
@@ -107,7 +107,7 @@ export const utilisateurApiClient: UtilisateurApiClient = {
       }
     `)({ email })
   },
-  removeUtilisateur: async (userId: string) => getWithJson('/rest/utilisateurs/:id/delete', { id: userId }),
+  removeUtilisateur: async (userId: UtilisateurId) => getWithJson('/rest/utilisateurs/:id/delete', { id: userId }),
   updateUtilisateur: async (utilisateur: UtilisateurToEdit) => postWithJson('/rest/utilisateurs/:id/permission', { id: utilisateur.id }, utilisateur),
   getQGISToken: async () => postWithJson('/rest/utilisateur/generateQgisToken', {}, undefined),
 }