diff --git a/src/database/models/ClassificationRoot.js b/src/database/models/ClassificationRoot.js
new file mode 100644
index 0000000000000000000000000000000000000000..fe18ca26a420073d2381f9e8a980562d64f5a224
--- /dev/null
+++ b/src/database/models/ClassificationRoot.js
@@ -0,0 +1,64 @@
+const BaseModel = require('./BaseModel')
+const timestamps = require('./properties/timestamps')
+
+class ClassificationRoot extends BaseModel {
+	static associate(models) {
+		this.belongsTo(models.Upload, { foreignKey: 'upload_id' })
+	}
+
+	toJSON() {
+		return {
+			metric_id: this.metric_id,
+			upload_id: this.upload_id,
+			image_id: this.image_id,
+			url: this.url,
+			status: this.status,
+		}
+	}
+
+}
+
+module.exports = (sequelize, DataTypes) => {
+	ClassificationRoot.init(Object.assign(
+		{
+			metric_id: {
+				type: DataTypes.UUID,
+				primaryKey: true,
+				defaultValue: DataTypes.UUIDV4,
+				validate: {
+					isUUID: 4,
+				},
+			},
+			upload_id: {
+				type: DataTypes.UUID,
+				validate: {
+					isUUID: 4,
+				},
+			},
+			image_id: {
+				type: DataTypes.UUID,
+				validate: {
+					isUUID: 4,
+				},
+			},
+			url: {
+				type: DataTypes.TEXT,
+			},
+			status: {
+				type: DataTypes.TEXT,
+			},
+			meta: {
+				type: DataTypes.JSONB,
+			},
+		},
+		timestamps(DataTypes),
+	), {
+		sequelize,
+		paranoid: true,
+		tableName: 'classification_roots',
+	})
+
+	return ClassificationRoot
+}
+
+module.exports.Model = ClassificationRoot
diff --git a/src/http/controllers/api/v2/classifications.js b/src/http/controllers/api/v2/classifications.js
new file mode 100644
index 0000000000000000000000000000000000000000..00f5d559e188691111c455f9e23e6a035d0683de
--- /dev/null
+++ b/src/http/controllers/api/v2/classifications.js
@@ -0,0 +1,71 @@
+const publicStatus = new Set(['accepted'])
+const privateStatus = new Set(['pending', 'rejected'])
+
+const { Sequelize, ClassificationRoot } = require('database/models')
+const NotFoundError = require("core/errors/NotFoundError");
+const UnauthorizedError = require("core/errors/UnauthorizedError");
+const InputValidationError = require("core/errors/InputValidationError");
+
+exports.listRoots = async ctx => {
+	const { status = '', limit = 25, after = null } = ctx.query
+
+	const requestedStatus = status.split(',')
+		.map(s => s.trim())
+		.filter(s => publicStatus.has(s) || privateStatus.has(s))
+
+	const query = {
+		order: ['metric_id', 'created_at'],
+		where: {
+			status: {
+				[Sequelize.Op.in]: requestedStatus,
+			}
+		},
+		limit,
+	}
+
+	const count = await ClassificationRoot.count(query)
+
+	if (after) {
+		query.where.metric_id = {
+			[Sequelize.Op.gt]: after,
+		}
+	}
+
+	const roots = await ClassificationRoot.findAll(query)
+
+	ctx.body = {
+		roots,
+		meta: {
+			total: count,
+		}
+	}
+}
+
+exports.putRootStatus = async ctx => {
+	const user = await ctx.services['core.auth'].getUser()
+
+	if (!user) {
+		throw new UnauthorizedError()
+	}
+
+	if (ctx.models.classification == null) {
+		throw new NotFoundError('Classification Root')
+	}
+
+	const { status } = ctx.request.body
+	const { classification } = ctx.models
+
+	if (!publicStatus.has(status) && !privateStatus.has(status)) {
+		throw new InputValidationError(['status'])
+	}
+
+	classification.status = status
+	classification.meta = {
+		approved_by: user.id,
+	}
+	await classification.save()
+
+	ctx.body = {
+		root: classification,
+	}
+}
\ No newline at end of file
diff --git a/src/http/params/classification.js b/src/http/params/classification.js
new file mode 100644
index 0000000000000000000000000000000000000000..23362fe0f1b472e75b82432c30a43ca96fb322d0
--- /dev/null
+++ b/src/http/params/classification.js
@@ -0,0 +1,7 @@
+const { ClassificationRoot } = require('database/models')
+
+module.exports = async (id, ctx, next) => {
+	ctx.models = ctx.models ?? {}
+	ctx.models.classification = await ClassificationRoot.findByPk(id)
+	return await next()
+}
diff --git a/src/http/routers/routes_v2.js b/src/http/routers/routes_v2.js
index 45691f1663a664f4481155ab33d3614e2eec492e..3a1e040f7ac4adcd73fe6b3ec11d6746ffbdd35a 100644
--- a/src/http/routers/routes_v2.js
+++ b/src/http/routers/routes_v2.js
@@ -54,6 +54,7 @@ router.put('/uploads/:upload_id/:property', noop)
 
 router.param('survey', param('survey'))
 router.param('excerpt', param('survey_excerpt'))
+router.param('classification', param('classification'))
 
 router.get('/surveys', controller('api/v2/surveys', 'list'))
 router.get('/excerpts', controller('api/v2/surveys', 'listExcerpts'))
@@ -65,6 +66,9 @@ if (config('app.dev')) {
 	router.post('/surveys/factory', safemode, controller('api/v2/factories', 'survey'))
 }
 
+router.get('/classifications/roots', controller('api/v2/classifications', 'listRoots'))
+router.put('/classifications/roots/:classification/status', controller('api/v2/classifications', 'putRootStatus'))
+
 router.post('/an/ev', safemode, controller('api/analytics', 'track'))
 
 module.exports = router