diff --git a/api.v1.yaml b/api.v1.yaml
index 445d38e..67c18f5 100644
--- a/api.v1.yaml
+++ b/api.v1.yaml
@@ -443,6 +443,10 @@ paths:
       responses:
         '201':
           description: Created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/tag_full'
         '400':
           description: Bad Request
       requestBody:
@@ -474,16 +478,7 @@ paths:
           content:
             application/json:
               schema:
-                type: object
-                properties:
-                  relays:
-                    type: array
-                    items:
-                      $ref: '#/components/schemas/relay'
-                  schedules:
-                    type: array
-                    items:
-                      $ref: '#/components/schemas/schedule'
+                $ref: '#/components/schemas/tag_full'
         '404':
           description: Not Found
       operationId: get-tags-tag
@@ -822,6 +817,20 @@ components:
       type: string
       title: tag
       example: sprinkler
+    tag_full:
+      title: tag (full)
+      type: object
+      properties:
+        tag:
+          $ref: '#/components/schemas/tag'
+        relays:
+          type: array
+          items:
+            $ref: '#/components/schemas/relay'
+        schedules:
+          type: array
+          items:
+            $ref: '#/components/schemas/schedule'
     schedule_id:
       type: string
       title: schedule_id
diff --git a/emgauwa-core/src/handlers/v1/tags.rs b/emgauwa-core/src/handlers/v1/tags.rs
index 5b03038..851bf98 100644
--- a/emgauwa-core/src/handlers/v1/tags.rs
+++ b/emgauwa-core/src/handlers/v1/tags.rs
@@ -1,6 +1,8 @@
-use actix_web::{get, web, HttpResponse};
+use actix_web::{delete, get, post, web, HttpResponse};
 use emgauwa_lib::db::DbTag;
-use emgauwa_lib::errors::EmgauwaError;
+use emgauwa_lib::errors::{DatabaseError, EmgauwaError};
+use emgauwa_lib::models::{FromDbModel, Tag};
+use emgauwa_lib::types::RequestCreateTag;
 use sqlx::{Pool, Sqlite};
 
 #[get("/api/v1/tags")]
@@ -13,3 +15,47 @@ pub async fn index(pool: web::Data<Pool<Sqlite>>) -> Result<HttpResponse, Emgauw
 
 	Ok(HttpResponse::Ok().json(tags))
 }
+
+#[get("/api/v1/tags/{tag_name}")]
+pub async fn show(
+	pool: web::Data<Pool<Sqlite>>,
+	path: web::Path<(String,)>,
+) -> Result<HttpResponse, EmgauwaError> {
+	let mut pool_conn = pool.acquire().await?;
+
+	let (tag_name,) = path.into_inner();
+
+	let tag = DbTag::get_by_tag(&mut pool_conn, &tag_name)
+		.await?
+		.ok_or(DatabaseError::NotFound)?;
+
+	let return_tag = Tag::from_db_model(&mut pool_conn, tag)?;
+	Ok(HttpResponse::Ok().json(return_tag))
+}
+
+#[delete("/api/v1/tags/{tag_name}")]
+pub async fn delete(
+	pool: web::Data<Pool<Sqlite>>,
+	path: web::Path<(String,)>,
+) -> Result<HttpResponse, EmgauwaError> {
+	let mut pool_conn = pool.acquire().await?;
+
+	let (tag_name,) = path.into_inner();
+
+	DbTag::delete_by_tag(&mut pool_conn, &tag_name).await?;
+	Ok(HttpResponse::Ok().json("tag got deleted"))
+}
+
+#[post("/api/v1/tags")]
+pub async fn add(
+	pool: web::Data<Pool<Sqlite>>,
+	data: web::Json<RequestCreateTag>,
+) -> Result<HttpResponse, EmgauwaError> {
+	let mut pool_conn = pool.acquire().await?;
+
+	let new_tag = DbTag::create(&mut pool_conn, &data.tag).await?;
+
+	let cache = (Vec::new(), Vec::new()); // a new tag can't have any relays or schedules
+	let return_tag = Tag::from_db_model_cache(&mut pool_conn, new_tag, cache)?;
+	Ok(HttpResponse::Created().json(return_tag))
+}
diff --git a/emgauwa-core/src/main.rs b/emgauwa-core/src/main.rs
index 1f3e1dd..1191da6 100644
--- a/emgauwa-core/src/main.rs
+++ b/emgauwa-core/src/main.rs
@@ -85,6 +85,9 @@ async fn main() -> Result<(), std::io::Error> {
 			.service(handlers::v1::schedules::update)
 			.service(handlers::v1::schedules::delete)
 			.service(handlers::v1::tags::index)
+			.service(handlers::v1::tags::show)
+			.service(handlers::v1::tags::delete)
+			.service(handlers::v1::tags::add)
 			.service(handlers::v1::ws::ws_controllers)
 	})
 	.listen(listener)?
diff --git a/emgauwa-lib/src/db/tag.rs b/emgauwa-lib/src/db/tag.rs
index abd964d..c6a374b 100644
--- a/emgauwa-lib/src/db/tag.rs
+++ b/emgauwa-lib/src/db/tag.rs
@@ -63,4 +63,25 @@ impl DbTag {
 			.await
 			.map_err(DatabaseError::from)
 	}
+
+	pub async fn delete_by_tag(
+		conn: &mut PoolConnection<Sqlite>,
+		filter_tag: &str,
+	) -> Result<(), DatabaseError> {
+		if sqlx::query_scalar!("SELECT 1 FROM tags WHERE tag = ?", filter_tag)
+			.fetch_optional(conn.deref_mut())
+			.await?
+			.is_none()
+		{
+			return Err(DatabaseError::NotFound);
+		}
+
+		sqlx::query!("DELETE FROM tags WHERE tag = ?", filter_tag)
+			.execute(conn.deref_mut())
+			.await
+			.map(|res| match res.rows_affected() {
+				0 => Err(DatabaseError::DeleteError),
+				_ => Ok(()),
+			})?
+	}
 }
diff --git a/emgauwa-lib/src/models/mod.rs b/emgauwa-lib/src/models/mod.rs
index 7333c28..136de53 100644
--- a/emgauwa-lib/src/models/mod.rs
+++ b/emgauwa-lib/src/models/mod.rs
@@ -1,12 +1,14 @@
 mod controller;
 mod relay;
 mod schedule;
+mod tag;
 
 pub use controller::Controller;
 pub use relay::Relay;
 pub use schedule::Schedule;
 use sqlx::pool::PoolConnection;
 use sqlx::Sqlite;
+pub use tag::Tag;
 
 use crate::errors::DatabaseError;
 
diff --git a/emgauwa-lib/src/models/tag.rs b/emgauwa-lib/src/models/tag.rs
new file mode 100644
index 0000000..7e23fb4
--- /dev/null
+++ b/emgauwa-lib/src/models/tag.rs
@@ -0,0 +1,49 @@
+use actix::MessageResponse;
+use futures::executor::block_on;
+use serde_derive::{Deserialize, Serialize};
+use sqlx::pool::PoolConnection;
+use sqlx::Sqlite;
+
+use crate::db::{DbRelay, DbSchedule, DbTag};
+use crate::errors::DatabaseError;
+use crate::models::{convert_db_list, FromDbModel, Relay, Schedule};
+
+#[derive(Serialize, Deserialize, Debug, Clone, MessageResponse)]
+pub struct Tag {
+	pub tag: String,
+	pub relays: Vec<Relay>,
+	pub schedules: Vec<Schedule>,
+}
+
+impl FromDbModel for Tag {
+	type DbModel = DbTag;
+	type DbModelCache = (Vec<Relay>, Vec<Schedule>);
+
+	fn from_db_model(
+		conn: &mut PoolConnection<Sqlite>,
+		db_model: Self::DbModel,
+	) -> Result<Self, DatabaseError> {
+		let db_schedules = block_on(DbSchedule::get_by_tag(conn, &db_model))?;
+		let schedules: Vec<Schedule> = convert_db_list(conn, db_schedules)?;
+
+		let db_relays = block_on(DbRelay::get_by_tag(conn, &db_model))?;
+		let relays: Vec<Relay> = convert_db_list(conn, db_relays)?;
+
+		let cache = (relays, schedules);
+		Self::from_db_model_cache(conn, db_model, cache)
+	}
+
+	fn from_db_model_cache(
+		_conn: &mut PoolConnection<Sqlite>,
+		db_model: Self::DbModel,
+		cache: Self::DbModelCache,
+	) -> Result<Self, DatabaseError> {
+		let tag = db_model.tag.clone();
+		let (relays, schedules) = cache;
+		Ok(Tag {
+			tag,
+			relays,
+			schedules,
+		})
+	}
+}
diff --git a/emgauwa-lib/src/types/request.rs b/emgauwa-lib/src/types/request.rs
index 646b49a..036cf9d 100644
--- a/emgauwa-lib/src/types/request.rs
+++ b/emgauwa-lib/src/types/request.rs
@@ -38,6 +38,11 @@ pub struct RequestUpdateController {
 	pub name: String,
 }
 
+#[derive(Debug, Serialize, Deserialize)]
+pub struct RequestCreateTag {
+	pub tag: String,
+}
+
 impl RequestScheduleId {
 	pub async fn get_schedule(
 		&self,