diff --git a/ezcache/territoires_stats.php b/ezcache/territoires_stats.php
new file mode 100644
index 0000000000000000000000000000000000000000..34dddeea2806de8e81d320470863bf8a37a3c73f
--- /dev/null
+++ b/ezcache/territoires_stats.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Ce fichier contient les fonctions de service nécessitées par le plugin Cache Factory.
+ */
+if (!defined('_ECRIRE_INC_VERSION')) {
+	return;
+}
+
+/**
+ * Renvoie la configuration spécifique des caches gérés par REST Factory si les fonctions de collection des données
+ * sont directement codés en PHP.
+ *
+ * Dans le cas où les données JSON sont créées via des squelettes SPIP, le cache est déjà géré par SPIP.
+ *
+ * @param string $plugin Identifiant qui permet de distinguer le module appelant qui peut-être un plugin comme le noiZetier ou
+ *                       un script. Pour un plugin, le plus pertinent est d'utiliser le préfixe.
+ *
+ * @return array<string, mixed> Tableau de la configuration brute du plugin Taxonomie.
+ */
+function territoires_stats_cache_configurer(string $plugin) : array {
+	// Initialisation du tableau de configuration avec les valeurs par défaut du plugin Cache.
+	$configuration = [
+		'config' => [
+			'racine'          => '_DIR_ETC',
+			'sous_dossier'    => true,
+			'nom_prefixe'     => 'config',
+			'nom_obligatoire' => [],
+			'nom_facultatif'  => [],
+			'extension'       => '.yaml',
+			'securisation'    => false,
+			'serialisation'   => false,
+			'decodage'        => true,
+			'separateur'      => '',
+			'conservation'    => 0,
+			'administration'  => false
+		],
+		'source' => [
+			'racine'          => '_DIR_ETC',
+			'sous_dossier'    => true,
+			'nom_prefixe'     => 'source_1',
+			'nom_obligatoire' => [],
+			'nom_facultatif'  => [],
+			'extension'       => '.json',
+			'securisation'    => false,
+			'serialisation'   => false,
+			'decodage'        => true,
+			'separateur'      => '-',
+			'conservation'    => 0,
+			'administration'  => false
+		],
+	];
+
+	return $configuration;
+}
diff --git a/ezmashup/config.template.yaml b/ezmashup/config.template.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5cffe0b0089698776aaab39abd9de8879a9e8489
--- /dev/null
+++ b/ezmashup/config.template.yaml
@@ -0,0 +1,81 @@
+# Ce template permet au plugin d'initialiser les YAML utilisateur.
+# Il contient tous les champs possibles.
+# ---------------------------------------------------------------
+
+# L'identifiant du feed est le nom du dossier contenant le fichier YAML
+title: ''
+description: ''
+
+# Catégorisation du feed
+# -- Pour le plugin, la catégorie des feeds est toujours la même, `territory_data`
+category: 'territory_data'
+# -- Le plugin utilise les tags pour définir le type de territoire, le pays et le code territoire utilisé dans le jeu de données
+tags:
+  type_id: ''
+  type: ''
+  pays: ''
+
+# Configuration du stockage de destination : c'est la table `spip_territoires_extras`
+target:
+  format: 'sql_table'
+  id: 'territoires_extras'
+  # Options de paramétrage de la cible :
+  # - le plugin définit une limite à 5000 enregistrements par insertion (protection pour timeout)
+  options:
+    max_chunk: 5000
+
+# Configuration du mapping et du processus de mashup
+mapping:
+  # Données de base : ces données sont issues du jeu de données
+  # - `code_feed` contient le code utilisé par jeu si celui-ci n'est pas le code primaire du territoire. Sinon ce champ
+  #   est absent et on remplir directement le champ `iso_territoire`
+  basic_fields:
+    iso_territoire: 'iso_feed'
+    code_feed: ''
+    valeur: ''
+
+  # Liste des champs de base étendus : ces données sont calculées à partir des champs de base et inclus dans le stockage
+  # - aucun nécessaire
+
+  # Données statiques : à ajouter à la target mais ne provient pas d'un dataset source
+  # Le plugin définit les champs qui seront inclus dans la table spip hors le code de territoire et la valeur :
+  # - l'identifiant de la données (extra), le type d'extra à savoir toujours `stat` pour le plugin, l'id du feed,
+  #   le type de territoire et le pays (proviennent des tags)
+  static_fields:
+    extra: ''
+    type_extra: 'stat'
+    feed_id: '/feed_id'
+    type: '#type'
+    iso_pays: '#pays'
+
+  # Liste des champs de la target qui ne seront pas retenus pour le stockage
+  # - Si `code_feed` est utilisé, sinon vide
+  unused_fields: ['code_feed']
+
+# Le plugin ne peut pas utiliser d'include (aucune fonction particulière par feed n'est possible)
+
+# Description des datasets source primaires
+# - Le plugin ne supporte qu'un seul dataset primaire et aucun addon.
+sources_basic:
+  source_1:
+    # Complément pour la source
+    source:
+      # Prend les valeurs file ou api
+      type: ''
+      # Définit le format du fichier (json, xml ou csv) ou du contenu retourné par l'api : json uniquement.
+      format: ''
+      # Nom du fichier avec extension ou URL HTTP
+      uri: ''
+      last_update: ''
+      version: ''
+      license: ''
+    # Informations optionnelles permettant de décoder le contenu de la source
+    decoding:
+      # - root_node : sommet de l'arborescence à extraire (sous la forme a/b/c) si la source n'est pas un fichier CSV (vide par défaut)
+      root_node: ''
+      # - delimiter : si la source est un fichier CSV (virgule par défaut)
+      delimiter: ''
+    # Description du provider pour les crédits
+    provider:
+      name: ''
+      url: ''
diff --git a/ezmashup/territoires_stats.php b/ezmashup/territoires_stats.php
index 3d014b9677fc0e97a1ba3db768d25498a190c733..c2260a4e6c3cb82d2e2cd751f1bccebcf562b40a 100644
--- a/ezmashup/territoires_stats.php
+++ b/ezmashup/territoires_stats.php
@@ -17,12 +17,10 @@ if (!defined('_ECRIRE_INC_VERSION')) {
  *
  * @pipeline_appel feed_lister_categories
  *
- * @param string $plugin Préfixe du plugin utilisateur.
- *
  * @return array Liste des catégories et de leur description au format [id] = tableau de description avec le nom (label),
  *               la description et l'icone.
  */
-function territoires_stats_feed_lister_categories(string $plugin) : array {
+function territoires_stats_feed_lister_categories() : array {
 	// Initialisation des catégories par défaut
 	return [
 		'territory_data' => [
@@ -36,11 +34,9 @@ function territoires_stats_feed_lister_categories(string $plugin) : array {
 /**
  * Renvoie la catégorie par défaut dans la liste des catégories supportées par le plugin.
  *
- * @param string $plugin Préfixe du plugin utilisateur.
- *
  * @return string Identifiant de la catégorie par défaut.
  */
-function territoires_stats_feed_initialiser_categorie_defaut(string $plugin) : string {
+function territoires_stats_feed_initialiser_categorie_defaut() : string {
 	// Initialisation des catégories par défaut de Mashup Factory
 	return 'territory_data';
 }
@@ -49,17 +45,15 @@ function territoires_stats_feed_initialiser_categorie_defaut(string $plugin) : s
  * Renvoie la configuration par défaut de la racine où trouver les feeds.
  * Cette information est utilisée a minima au chargement des feeds disponibles.
  *
- * Le plugin crée dynamiquement les feeds. De fait, il les stocke dans le
- * dossier des données permanentes inaccessibles, _DIR_ETC. Ce dossier n'étant pas accessible via le path de spip
- * il est nécessaire d'indiquer la racine exacte qui fera l'objet d'un traitement particulier.
- *
- * @param string $plugin Préfixe du plugin utilisateur.
+ * Le plugin crée dynamiquement les feeds. De fait, il les stocke dans le dossier des données permanentes inaccessibles,
+ * _DIR_ETC. Ce dossier n'étant pas accessible via le path de spip il est nécessaire d'indiquer la racine exacte qui
+ * fera l'objet d'un traitement particulier.
  *
  * @return string Racine où chercher les feeds.
  */
-function territoires_stats_feed_initialiser_racine(string $plugin) : string {
+function territoires_stats_feed_initialiser_racine() : string {
 	// Le plugin range les feeds créés dynamiquement dans _DIR_ETC.
-	return _DIR_ETC . $plugin . '/';
+	return _DIR_ETC;
 }
 
 /**
@@ -67,12 +61,9 @@ function territoires_stats_feed_initialiser_racine(string $plugin) : string {
  *
  * Le plugin rajoute la consignation des données statistiques.
  *
- * @package SPIP\MASHUP\SERVICE
- *
  * @uses unite_peuplement_consigne_identifier()
- * @uses ezmashup_chercher_service()
  *
- * @param array  $feed   Configuration du feed
+ * @param array $feed Configuration du feed
  *
  * @return void
  */
@@ -109,11 +100,9 @@ function territoires_stats_feed_completer_execution(array $feed) : void {
  *
  * Le plugin Mashup Factory ne fait rien de particulier actuellement.
  *
- * @package SPIP\MASHUP\SERVICE
- *
- * @uses ezmashup_chercher_service()
+ * @uses unite_peuplement_consigne_identifier()
  *
- * @param array  $feed   Configuration du feed
+ * @param array $feed Configuration du feed
  *
  * @return void
  */
@@ -160,8 +149,8 @@ function territoires_stats_feed_completer_vidage(array $feed) : void {
  * Le plugin gère les cas où le code identifiant les territoires ne sont pas les codes ISO comme l'attend la
  * table spip_territoires_extras (par exemple, le code INSEE en France).
  *
- * @param array  $item   Item d'un dataset source
- * @param array  $feed   Configuration du feed
+ * @param array $item Item d'un dataset source
+ * @param array $feed Configuration du feed
  *
  * @return array Item mis à jour
  */
diff --git a/formulaires/creer_feed_territoires.php b/formulaires/creer_feed_territoires.php
index 0d070b9e6c9ce352d46a487f15b117dcc753ddb8..572f3ac07647de4ee96f8fae897346f43b6e27ba 100644
--- a/formulaires/creer_feed_territoires.php
+++ b/formulaires/creer_feed_territoires.php
@@ -186,7 +186,7 @@ function formulaires_creer_feed_territoires_verifier_1() : array {
 			// Extraire l'extension pour déterminer l'option de décodage à présenter
 			[, $format] = explode('/', $_FILES[$variable_source]['type']);
 		}
-		set_request('format_source', $format);
+		set_request('_format_source', $format);
 		if ($format === 'csv') {
 			set_request('_label_decodage', _T('territoires_stats:label_feed_decodage_delimiteur'));
 			set_request('_explication_decodage', _T('territoires_stats:explication_feed_decodage_delimiteur'));
@@ -197,12 +197,10 @@ function formulaires_creer_feed_territoires_verifier_1() : array {
 
 		// -- Identifiant proposé pour le feed
 		include_spip('inc/ezmashup_feed');
-		$prefixe = "{$type}_{$pays}_{$extra}";
-		foreach (['', '2', '3', '4', '5', '6', '7', '8', '9'] as $_suffixe) {
-			$id_feed = $prefixe . ($_suffixe ? '_' : '') . $_suffixe;
-			if (!feed_yaml_existe('territoires_stats', $id_feed)) {
-				break;
-			}
+		$id_feed = "{$type}_" . ($pays ? "{$pays}_" : '') . $extra;
+		if (feed_ressource_existe('territoires_stats', $id_feed, 'config')) {
+			// On rajoute un suffixe que l'utilisateur devra modifier
+			$id_feed .= '_xxx';
 		}
 		set_request('_feed_id_defaut', strtolower($id_feed));
 	}
@@ -253,7 +251,7 @@ function formulaires_creer_feed_territoires_verifier_2() : array {
 	include_spip('inc/ezmashup_feed');
 	if (!preg_match('#^[\w]+$#i', $id_feed)) {
 		$erreurs['feed_id'] = _T('territoires_stats:erreur_feed_id');
-	} elseif (feed_yaml_existe('territoires_stats', $id_feed)) {
+	} elseif (feed_ressource_existe('territoires_stats', $id_feed, 'config')) {
 		$erreurs['feed_id'] = _T('territoires_stats:erreur_feed_id_existe');
 	}
 
@@ -264,13 +262,115 @@ function formulaires_creer_feed_territoires_verifier_2() : array {
  * Exécution du formulaire : .
  *
  * @return array Tableau retourné par le formulaire contenant toujours un message de bonne exécution ou
- *               d'erreur. L'indicateur editable est toujours à vrai.
+ *               d'erreur.
  */
 function formulaires_creer_feed_territoires_traiter() : array {
 	// Initialisation du retour de la fonction
 	$retour = [];
 
-	// Récupération des paramètres de base de la carte et début de consolidation des champs de l'objet carte
+	// Récupération des variables constitutives du YAML
+	// -- étape 1
+	$type = _request('type');
+	$pays = _request('pays_' . $type) ?? '';
+	$type_source = _request('type_source');
+	$source = $type_source === 'api'
+		? _request('url_source')
+		: basename($_FILES['fichier_source']['name']);
+	$extra = _request('extra');
+//	$titre_extra = _request('titre_extra');
+	$format_source = _request('format_source');
+
+	// -- étape 2
+	$type_code = _request('type_code');
+	$decodage = trim(_request('decodage')) ?? '';
+	$mapping_code = _request('mapping_code');
+	$mapping_valeur = _request('mapping_valeur');
+	$id_feed = _request('feed_id');
+	$titre_feed = _request('titre');
+	$description_feed = _request('description') ?? '';
+
+	// Initialisation du YAML à partir du template
+	include_spip('inc/yaml');
+	$yaml_template = _DIR_PLUGIN_TERRITOIRES_STATS . 'ezmashup/config.template.yaml';
+	$description_yaml = yaml_decode_file($yaml_template);
+
+	// Personnalisation du YAML en fonction du contexte des saisies.
+	// On considère que toutes les vérifications ont été faites et que donc les variables saisies sont cohérentes
+	// -- identification
+	$description_yaml['title'] = $titre_feed;
+	$description_yaml['description'] = $description_feed;
+	// -- catégorisation : seuls les tags sont à mettre à jour
+	$description_yaml['tags']['type'] = $type;
+	$description_yaml['tags']['pays'] = $pays;
+	$description_yaml['tags']['type_id'] = $type_code;
+	// -- mapping : basic, static et unused fields
+	if ($type_code === 'iso_territoire') {
+		// Le code primaire est utilisé dans jeu de données, il n'est pas utile de passer par un code alternatif
+		// - le champ `code_feed` est donc inutile
+		$description_yaml['mapping']['basic_fields']['iso_territoire'] = $mapping_code;
+		unset($description_yaml['mapping']['basic_fields']['code_feed']);
+		unset($description_yaml['mapping']['unused_fields']);
+	} else {
+		// Le jeu de données utilise un code alternatif au code primaire
+		// - le champ `code_feed` est donc nécessaire temporairement
+		$description_yaml['mapping']['basic_fields']['code_feed'] = $mapping_code;
+	}
+	$description_yaml['mapping']['basic_fields']['valeur'] = $mapping_valeur;
+	$description_yaml['mapping']['static_fields']['extra'] = $extra;
+	// -- la source : pour l'instant les crédits ne sont pas supportés, elle porte toujours l'id `source_1`
+	$description_yaml['sources_basic']['source_1']['source']['type'] = $type_source;
+	$description_yaml['sources_basic']['source_1']['source']['format'] = $format_source;
+	$description_yaml['sources_basic']['source_1']['source']['uri'] = $source;
+	if ($format_source === 'csv') {
+		unset($description_yaml['sources_basic']['source_1']['decoding']['root_node']);
+		$description_yaml['sources_basic']['source_1']['decoding']['delimiter'] = $decodage;
+	} else {
+		unset($description_yaml['sources_basic']['source_1']['decoding']['delimiter']);
+		$description_yaml['sources_basic']['source_1']['decoding']['root_node'] = $decodage;
+	}
+
+	// Ecriture de la source si elle est au format fichier
+	if ($type_source === 'file') {
+		// Récupération du fichier qui est stocké temporairement dans tmp/ en attendant le chargement du feed
+		// -- Création du répertoire d'upload
+		$dir = sous_repertoire(_DIR_TMP, 'territoires_stats');
+
+		// -- Détermination du nom du fichier temporaire de façon à ce qu'il soit unique.
+		$hash = md5("{$format_source}-" . $GLOBALS['visiteur_session']['id_auteur'] . time());
+		$fichier = $dir . $hash . '-' . $_FILES['fichier_source']['name'];
+
+		// -- Déplacement du fichier téléchargé dans la destination choisie.
+		if (move_uploaded_file($_FILES['fichier_source']['tmp_name'], $fichier)) {
+			// -- Lecture et suppression du fichier temporaire
+			include_spip('inc/flock');
+			lire_fichier($fichier, $contenu_source);
+			@unlink($fichier);
+
+			// Stockage de la source dans son emplacement final
+			if (!feed_ressource_ecrire('territoires_stats', $id_feed, 'source', $contenu_source)) {
+				$retour['message_erreur'] = _T('territoires_stats:erreur_recupération_source');
+			}
+		}
+		else {
+			$retour['message_erreur'] = _T('territoires_stats:erreur_recupération_source');
+		}
+	}
+
+	if (empty($retour['message_erreur'])) {
+		// Ecriture du YAML
+		include_spip('inc/ezmashup_feed');
+		if (feed_ressource_ecrire('territoires_stats', $id_feed, 'config', $description_yaml)) {
+			// Insertion du YAML en base de données
+			feed_charger('territoires_stats');
+		} else {
+			$retour['message_erreur'] = _T('territoires_stats:erreur_ecriture_config');
+		}
+	}
+
+	// Redirection vers la page des jeux de données si tout s'est bien passé
+	if (empty($retour['message_erreur'])) {
+		$retour['redirect'] = generer_url_ecrire('peupler_statistiques');
+	}
 
 	return $retour;
 }
diff --git a/formulaires/creer_feed_territoires_2.html b/formulaires/creer_feed_territoires_2.html
index 0d40933b67b3cd89b4180f78af8d8d4f0d638abc..801ba1784216f2872d6bf9a0765ab360e26655c4 100644
--- a/formulaires/creer_feed_territoires_2.html
+++ b/formulaires/creer_feed_territoires_2.html
@@ -59,6 +59,10 @@
 				})]
 		</fieldset>
 
+		[(#SAISIE{hidden, format_source,
+			defaut=#ENV{_format_source}
+		})]
+
 		<p class="boutons">
 			#SET{etape_precedente, #ENV{_etape}|moins{1}}
 			<input type="submit" class="submit" name="_retour_etape_1" value="<:territoires:info_etape_precedente:>" />
diff --git a/paquet.xml b/paquet.xml
index d19360523f461306119ccfebb44ffdb79144c72f..c507cea2b9a7ebdb9fc26fd1f9cfd716c4277863 100644
--- a/paquet.xml
+++ b/paquet.xml
@@ -19,4 +19,5 @@
 
 	<necessite nom="ezmashup" compatibilite="[1.0.2;]" />
     <necessite nom="territoires" compatibilite="[1.5.6;]" />
+    <necessite nom="ezcache" compatibilite="[1.3.1;[" />
 </paquet>