summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-07-28 08:24:15 +1000
committerMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2023-07-28 08:24:15 +1000
commit8a3989c29776ad7eba8077bf7cc9c56e28b9b8c3 (patch)
treee71d68052e6bf5ddfad64c643a8fb2d04c2e183c /src
parentMerge branch 'master' into feat/refactorIdentify (diff)
parentMerge pull request #1075 from SpecificProtagonist/get_messages_around (diff)
downloadserver-8a3989c29776ad7eba8077bf7cc9c56e28b9b8c3.tar.xz
Merge branch 'master' into feat/refactorIdentify
Diffstat (limited to 'src')
-rw-r--r--src/api/routes/applications/#id/bot/index.ts117
-rw-r--r--src/api/routes/applications/#id/entitlements.ts22
-rw-r--r--src/api/routes/applications/#id/index.ts91
-rw-r--r--src/api/routes/applications/#id/skus.ts18
-rw-r--r--src/api/routes/applications/detectable.ts20
-rw-r--r--src/api/routes/applications/index.ts45
-rw-r--r--src/api/routes/auth/forgot.ts13
-rw-r--r--src/api/routes/auth/generate-registration-tokens.ts19
-rw-r--r--src/api/routes/auth/location-metadata.ts35
-rw-r--r--src/api/routes/auth/login.ts12
-rw-r--r--src/api/routes/auth/logout.ts33
-rw-r--r--src/api/routes/auth/mfa/totp.ts18
-rw-r--r--src/api/routes/auth/mfa/webauthn.ts8
-rw-r--r--src/api/routes/auth/register.ts46
-rw-r--r--src/api/routes/auth/reset.ts13
-rw-r--r--src/api/routes/auth/verify/index.ts13
-rw-r--r--src/api/routes/auth/verify/resend.ts13
-rw-r--r--src/api/routes/auth/verify/view-backup-codes-challenge.ts12
-rw-r--r--src/api/routes/channels/#channel_id/index.ts40
-rw-r--r--src/api/routes/channels/#channel_id/invites.ts36
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/ack.ts10
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts11
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/index.ts125
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts66
-rw-r--r--src/api/routes/channels/#channel_id/messages/bulk-delete.ts18
-rw-r--r--src/api/routes/channels/#channel_id/messages/index.ts277
-rw-r--r--src/api/routes/channels/#channel_id/permissions.ts14
-rw-r--r--src/api/routes/channels/#channel_id/pins.ts42
-rw-r--r--src/api/routes/channels/#channel_id/purge.ts22
-rw-r--r--src/api/routes/channels/#channel_id/recipients.ts142
-rw-r--r--src/api/routes/channels/#channel_id/typing.ts13
-rw-r--r--src/api/routes/channels/#channel_id/webhooks.ts42
-rw-r--r--src/api/routes/connections/#connection_name/callback.ts2
-rw-r--r--src/api/routes/discoverable-guilds.ts84
-rw-r--r--src/api/routes/discovery.ts34
-rw-r--r--src/api/routes/download.ts47
-rw-r--r--src/api/routes/gateway/bot.ts44
-rw-r--r--src/api/routes/gateway/index.ts30
-rw-r--r--src/api/routes/gifs/search.ts66
-rw-r--r--src/api/routes/gifs/trending-gifs.ts61
-rw-r--r--src/api/routes/gifs/trending.ts178
-rw-r--r--src/api/routes/guild-recommendations.ts52
-rw-r--r--src/api/routes/guilds/#guild_id/bans.ts85
-rw-r--r--src/api/routes/guilds/#guild_id/channels.ts58
-rw-r--r--src/api/routes/guilds/#guild_id/delete.ts60
-rw-r--r--src/api/routes/guilds/#guild_id/discovery-requirements.ts70
-rw-r--r--src/api/routes/guilds/#guild_id/emojis.ts110
-rw-r--r--src/api/routes/guilds/#guild_id/index.ts85
-rw-r--r--src/api/routes/guilds/#guild_id/invites.ts11
-rw-r--r--src/api/routes/guilds/#guild_id/member-verification.ts26
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/index.ts212
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/nick.ts15
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts20
-rw-r--r--src/api/routes/guilds/#guild_id/members/index.ts67
-rw-r--r--src/api/routes/guilds/#guild_id/messages/search.ts253
-rw-r--r--src/api/routes/guilds/#guild_id/profile/index.ts15
-rw-r--r--src/api/routes/guilds/#guild_id/prune.ts55
-rw-r--r--src/api/routes/guilds/#guild_id/regions.ts37
-rw-r--r--src/api/routes/guilds/#guild_id/roles/#role_id/index.ts77
-rw-r--r--src/api/routes/guilds/#guild_id/roles/index.ts50
-rw-r--r--src/api/routes/guilds/#guild_id/stickers.ts100
-rw-r--r--src/api/routes/guilds/#guild_id/templates.ts73
-rw-r--r--src/api/routes/guilds/#guild_id/vanity-url.ts46
-rw-r--r--src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts18
-rw-r--r--src/api/routes/guilds/#guild_id/welcome-screen.ts40
-rw-r--r--src/api/routes/guilds/#guild_id/widget.json.ts143
-rw-r--r--src/api/routes/guilds/#guild_id/widget.png.ts294
-rw-r--r--src/api/routes/guilds/#guild_id/widget.ts49
-rw-r--r--src/api/routes/guilds/index.ts28
-rw-r--r--src/api/routes/guilds/templates/index.ts119
-rw-r--r--src/api/routes/invites/index.ts125
-rw-r--r--src/api/routes/oauth2/authorize.ts234
-rw-r--r--src/api/routes/ping.ts44
-rw-r--r--src/api/routes/policies/instance/domains.ts41
-rw-r--r--src/api/routes/policies/instance/index.ts20
-rw-r--r--src/api/routes/policies/instance/limits.ts20
-rw-r--r--src/api/routes/policies/stats.ts41
-rw-r--r--src/api/routes/read-states/ack-bulk.ts12
-rw-r--r--src/api/routes/science.ts18
-rw-r--r--src/api/routes/sticker-packs/index.ts22
-rw-r--r--src/api/routes/stickers/#sticker_id/index.ts22
-rw-r--r--src/api/routes/stop.ts12
-rw-r--r--src/api/routes/updates.ts62
-rw-r--r--src/api/routes/users/#id/delete.ts13
-rw-r--r--src/api/routes/users/#id/index.ts22
-rw-r--r--src/api/routes/users/#id/profile.ts54
-rw-r--r--src/api/routes/users/#id/relationships.ts16
-rw-r--r--src/api/routes/users/@me/channels.ts51
-rw-r--r--src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts2
-rw-r--r--src/api/routes/users/@me/delete.ts69
-rw-r--r--src/api/routes/users/@me/disable.ts61
-rw-r--r--src/api/routes/users/@me/guilds.ts133
-rw-r--r--src/api/routes/users/@me/guilds/#guild_id/settings.ts40
-rw-r--r--src/api/routes/users/@me/index.ts59
-rw-r--r--src/api/routes/users/@me/mfa/codes-verification.ts21
-rw-r--r--src/api/routes/users/@me/mfa/codes.ts22
-rw-r--r--src/api/routes/users/@me/mfa/totp/disable.ts22
-rw-r--r--src/api/routes/users/@me/mfa/totp/enable.ts23
-rw-r--r--src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts34
-rw-r--r--src/api/routes/users/@me/mfa/webauthn/credentials/index.ts12
-rw-r--r--src/api/routes/users/@me/notes.ts134
-rw-r--r--src/api/routes/users/@me/relationships.ts195
-rw-r--r--src/api/routes/users/@me/settings.ts46
-rw-r--r--src/api/routes/voice/regions.ts21
-rw-r--r--src/api/util/handlers/route.ts51
-rw-r--r--src/api/util/utility/ipAddress.ts2
-rw-r--r--src/bundle/Server.ts4
-rw-r--r--src/gateway/events/Connection.ts2
-rw-r--r--src/gateway/opcodes/LazyRequest.ts31
-rw-r--r--src/util/config/types/GeneralConfiguration.ts1
-rw-r--r--src/util/config/types/SecurityConfiguration.ts2
-rw-r--r--src/util/config/types/subconfigurations/limits/RateLimits.ts4
-rw-r--r--src/util/entities/Channel.ts25
-rw-r--r--src/util/entities/Guild.ts44
-rw-r--r--src/util/entities/Member.ts10
-rw-r--r--src/util/entities/User.ts24
-rw-r--r--src/util/interfaces/Activity.ts2
-rw-r--r--src/util/interfaces/GuildWelcomeScreen.ts10
-rw-r--r--src/util/interfaces/index.ts1
-rw-r--r--src/util/schemas/AckBulkSchema.ts12
-rw-r--r--src/util/schemas/IdentifySchema.ts10
-rw-r--r--src/util/schemas/LazyRequestSchema.ts9
-rw-r--r--src/util/schemas/LoginResponse.ts14
-rw-r--r--src/util/schemas/MemberChangeProfileSchema.ts3
-rw-r--r--src/util/schemas/UserGuildSettingsSchema.ts4
-rw-r--r--src/util/schemas/UserNoteUpdateSchema.ts3
-rw-r--r--src/util/schemas/UserProfileModifySchema.ts3
-rw-r--r--src/util/schemas/UserProfileResponse.ts26
-rw-r--r--src/util/schemas/UserRelationsResponse.ts27
-rw-r--r--src/util/schemas/VoiceStateUpdateSchema.ts2
-rw-r--r--src/util/schemas/WebAuthnSchema.ts6
-rw-r--r--src/util/schemas/index.ts6
-rw-r--r--src/util/schemas/responses/APIErrorOrCaptchaResponse.ts6
-rw-r--r--src/util/schemas/responses/APIErrorResponse.ts12
-rw-r--r--src/util/schemas/responses/BackupCodesChallengeResponse.ts4
-rw-r--r--src/util/schemas/responses/CaptchaRequiredResponse.ts5
-rw-r--r--src/util/schemas/responses/DiscoverableGuildsResponse.ts8
-rw-r--r--src/util/schemas/responses/GatewayBotResponse.ts10
-rw-r--r--src/util/schemas/responses/GatewayResponse.ts3
-rw-r--r--src/util/schemas/responses/GenerateRegistrationTokensResponse.ts3
-rw-r--r--src/util/schemas/responses/GuildBansResponse.ts10
-rw-r--r--src/util/schemas/responses/GuildCreateResponse.ts3
-rw-r--r--src/util/schemas/responses/GuildDiscoveryRequirements.ts23
-rw-r--r--src/util/schemas/responses/GuildMessagesSearchResponse.ts32
-rw-r--r--src/util/schemas/responses/GuildPruneResponse.ts7
-rw-r--r--src/util/schemas/responses/GuildRecommendationsResponse.ts6
-rw-r--r--src/util/schemas/responses/GuildVanityUrl.ts17
-rw-r--r--src/util/schemas/responses/GuildVoiceRegionsResponse.ts7
-rw-r--r--src/util/schemas/responses/GuildWidgetJsonResponse.ts21
-rw-r--r--src/util/schemas/responses/GuildWidgetSettingsResponse.ts6
-rw-r--r--src/util/schemas/responses/InstanceDomainsResponse.ts6
-rw-r--r--src/util/schemas/responses/InstancePingResponse.ts13
-rw-r--r--src/util/schemas/responses/InstanceStatsResponse.ts8
-rw-r--r--src/util/schemas/responses/LocationMetadataResponse.ts5
-rw-r--r--src/util/schemas/responses/MemberJoinGuildResponse.ts8
-rw-r--r--src/util/schemas/responses/OAuthAuthorizeResponse.ts3
-rw-r--r--src/util/schemas/responses/Tenor.ts72
-rw-r--r--src/util/schemas/responses/TokenResponse.ts15
-rw-r--r--src/util/schemas/responses/TypedResponses.ts88
-rw-r--r--src/util/schemas/responses/UpdatesResponse.ts6
-rw-r--r--src/util/schemas/responses/UserNoteResponse.ts5
-rw-r--r--src/util/schemas/responses/UserProfileResponse.ts37
-rw-r--r--src/util/schemas/responses/UserRelationsResponse.ts7
-rw-r--r--src/util/schemas/responses/UserRelationshipsResponse.ts8
-rw-r--r--src/util/schemas/responses/WebAuthnCreateResponse.ts4
-rw-r--r--src/util/schemas/responses/WebhookCreateResponse.ts6
-rw-r--r--src/util/schemas/responses/index.ts34
-rw-r--r--src/util/util/Application.ts24
-rw-r--r--src/util/util/Gifs.ts25
-rw-r--r--src/util/util/index.ts2
-rw-r--r--src/webrtc/opcodes/SelectProtocol.ts4
-rw-r--r--src/webrtc/util/Constants.ts2
172 files changed, 4671 insertions, 2175 deletions
diff --git a/src/api/routes/applications/#id/bot/index.ts b/src/api/routes/applications/#id/bot/index.ts
index e3f1832c..3c431e3d 100644
--- a/src/api/routes/applications/#id/bot/index.ts
+++ b/src/api/routes/applications/#id/bot/index.ts
@@ -16,78 +16,99 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
 import {
 	Application,
-	generateToken,
-	User,
 	BotModifySchema,
-	handleFile,
 	DiscordApiErrors,
+	User,
+	createAppBotUser,
+	generateToken,
+	handleFile,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { verifyToken } from "node-2fa";
 
 const router: Router = Router();
 
-router.post("/", route({}), async (req: Request, res: Response) => {
-	const app = await Application.findOneOrFail({
-		where: { id: req.params.id },
-		relations: ["owner"],
-	});
-
-	if (app.owner.id != req.user_id)
-		throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
-
-	const user = await User.register({
-		username: app.name,
-		password: undefined,
-		id: app.id,
-		req,
-	});
-
-	user.id = app.id;
-	user.premium_since = new Date();
-	user.bot = true;
-
-	await user.save();
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {
+				body: "TokenOnlyResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const app = await Application.findOneOrFail({
+			where: { id: req.params.id },
+			relations: ["owner"],
+		});
 
-	// flags is NaN here?
-	app.assign({ bot: user, flags: app.flags || 0 });
+		if (app.owner.id != req.user_id)
+			throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
 
-	await app.save();
+		const user = await createAppBotUser(app, req);
 
-	res.send({
-		token: await generateToken(user.id),
-	}).status(204);
-});
+		res.send({
+			token: await generateToken(user.id),
+		}).status(204);
+	},
+);
 
-router.post("/reset", route({}), async (req: Request, res: Response) => {
-	const bot = await User.findOneOrFail({ where: { id: req.params.id } });
-	const owner = await User.findOneOrFail({ where: { id: req.user_id } });
+router.post(
+	"/reset",
+	route({
+		responses: {
+			200: {
+				body: "TokenResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const bot = await User.findOneOrFail({ where: { id: req.params.id } });
+		const owner = await User.findOneOrFail({ where: { id: req.user_id } });
 
-	if (owner.id != req.user_id)
-		throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+		if (owner.id != req.user_id)
+			throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
 
-	if (
-		owner.totp_secret &&
-		(!req.body.code || verifyToken(owner.totp_secret, req.body.code))
-	)
-		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+		if (
+			owner.totp_secret &&
+			(!req.body.code || verifyToken(owner.totp_secret, req.body.code))
+		)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
 
-	bot.data = { hash: undefined, valid_tokens_since: new Date() };
+		bot.data = { hash: undefined, valid_tokens_since: new Date() };
 
-	await bot.save();
+		await bot.save();
 
-	const token = await generateToken(bot.id);
+		const token = await generateToken(bot.id);
 
-	res.json({ token }).status(200);
-});
+		res.json({ token }).status(200);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "BotModifySchema" }),
+	route({
+		requestBody: "BotModifySchema",
+		responses: {
+			200: {
+				body: "Application",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as BotModifySchema;
 		if (!body.avatar?.trim()) delete body.avatar;
diff --git a/src/api/routes/applications/#id/entitlements.ts b/src/api/routes/applications/#id/entitlements.ts
index e88fb7f7..6388e6b3 100644
--- a/src/api/routes/applications/#id/entitlements.ts
+++ b/src/api/routes/applications/#id/entitlements.ts
@@ -16,15 +16,25 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), (req: Request, res: Response) => {
-	// TODO:
-	//const { exclude_consumed } = req.query;
-	res.status(200).send([]);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "ApplicationEntitlementsResponse",
+			},
+		},
+	}),
+	(req: Request, res: Response) => {
+		// TODO:
+		//const { exclude_consumed } = req.query;
+		res.status(200).send([]);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/applications/#id/index.ts b/src/api/routes/applications/#id/index.ts
index 067f5dad..c372869a 100644
--- a/src/api/routes/applications/#id/index.ts
+++ b/src/api/routes/applications/#id/index.ts
@@ -16,32 +16,55 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
 import {
 	Application,
-	DiscordApiErrors,
 	ApplicationModifySchema,
+	DiscordApiErrors,
 } from "@spacebar/util";
-import { verifyToken } from "node-2fa";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import { verifyToken } from "node-2fa";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const app = await Application.findOneOrFail({
-		where: { id: req.params.id },
-		relations: ["owner", "bot"],
-	});
-	if (app.owner.id != req.user_id)
-		throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "Application",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const app = await Application.findOneOrFail({
+			where: { id: req.params.id },
+			relations: ["owner", "bot"],
+		});
+		if (app.owner.id != req.user_id)
+			throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
 
-	return res.json(app);
-});
+		return res.json(app);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "ApplicationModifySchema" }),
+	route({
+		requestBody: "ApplicationModifySchema",
+		responses: {
+			200: {
+				body: "Application",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as ApplicationModifySchema;
 
@@ -73,23 +96,35 @@ router.patch(
 	},
 );
 
-router.post("/delete", route({}), async (req: Request, res: Response) => {
-	const app = await Application.findOneOrFail({
-		where: { id: req.params.id },
-		relations: ["bot", "owner"],
-	});
-	if (app.owner.id != req.user_id)
-		throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
+router.post(
+	"/delete",
+	route({
+		responses: {
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const app = await Application.findOneOrFail({
+			where: { id: req.params.id },
+			relations: ["bot", "owner"],
+		});
+		if (app.owner.id != req.user_id)
+			throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION;
 
-	if (
-		app.owner.totp_secret &&
-		(!req.body.code || verifyToken(app.owner.totp_secret, req.body.code))
-	)
-		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+		if (
+			app.owner.totp_secret &&
+			(!req.body.code ||
+				verifyToken(app.owner.totp_secret, req.body.code))
+		)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
 
-	await Application.delete({ id: app.id });
+		await Application.delete({ id: app.id });
 
-	res.send().status(200);
-});
+		res.send().status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/applications/#id/skus.ts b/src/api/routes/applications/#id/skus.ts
index fcb75423..dc4fad23 100644
--- a/src/api/routes/applications/#id/skus.ts
+++ b/src/api/routes/applications/#id/skus.ts
@@ -16,13 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	res.json([]).status(200);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "ApplicationSkusResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		res.json([]).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/applications/detectable.ts b/src/api/routes/applications/detectable.ts
index a8e30894..5cf9d171 100644
--- a/src/api/routes/applications/detectable.ts
+++ b/src/api/routes/applications/detectable.ts
@@ -16,14 +16,24 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	//TODO
-	res.send([]).status(200);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "ApplicationDetectableResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		//TODO
+		res.send([]).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/applications/index.ts b/src/api/routes/applications/index.ts
index 80a19aa8..5bba3338 100644
--- a/src/api/routes/applications/index.ts
+++ b/src/api/routes/applications/index.ts
@@ -16,28 +16,47 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
 import {
 	Application,
 	ApplicationCreateSchema,
-	trimSpecial,
+	Config,
 	User,
+	createAppBotUser,
+	trimSpecial,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const results = await Application.find({
-		where: { owner: { id: req.user_id } },
-		relations: ["owner", "bot"],
-	});
-	res.json(results).status(200);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIApplicationArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const results = await Application.find({
+			where: { owner: { id: req.user_id } },
+			relations: ["owner", "bot"],
+		});
+		res.json(results).status(200);
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "ApplicationCreateSchema" }),
+	route({
+		requestBody: "ApplicationCreateSchema",
+		responses: {
+			200: {
+				body: "Application",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as ApplicationCreateSchema;
 		const user = await User.findOneOrFail({ where: { id: req.user_id } });
@@ -51,7 +70,11 @@ router.post(
 			flags: 0,
 		});
 
-		await app.save();
+		// april 14, 2023: discord made bot users be automatically added to all new apps
+		const { autoCreateBotUsers } = Config.get().general;
+		if (autoCreateBotUsers) {
+			await createAppBotUser(app, req);
+		} else await app.save();
 
 		res.json(app);
 	},
diff --git a/src/api/routes/auth/forgot.ts b/src/api/routes/auth/forgot.ts
index e240dff2..6fa86021 100644
--- a/src/api/routes/auth/forgot.ts
+++ b/src/api/routes/auth/forgot.ts
@@ -30,7 +30,18 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "ForgotPasswordSchema" }),
+	route({
+		requestBody: "ForgotPasswordSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorOrCaptchaResponse",
+			},
+			500: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { login, captcha_key } = req.body as ForgotPasswordSchema;
 
diff --git a/src/api/routes/auth/generate-registration-tokens.ts b/src/api/routes/auth/generate-registration-tokens.ts
index 723875f8..80fdaed1 100644
--- a/src/api/routes/auth/generate-registration-tokens.ts
+++ b/src/api/routes/auth/generate-registration-tokens.ts
@@ -16,7 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { route, random } from "@spacebar/api";
+import { random, route } from "@spacebar/api";
 import { Config, ValidRegistrationToken } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 
@@ -25,7 +25,22 @@ export default router;
 
 router.get(
 	"/",
-	route({ right: "OPERATOR" }),
+	route({
+		query: {
+			count: {
+				type: "number",
+				description:
+					"The number of registration tokens to generate. Defaults to 1.",
+			},
+			length: {
+				type: "number",
+				description:
+					"The length of each registration token. Defaults to 255.",
+			},
+		},
+		right: "OPERATOR",
+		responses: { 200: { body: "GenerateRegistrationTokensResponse" } },
+	}),
 	async (req: Request, res: Response) => {
 		const count = req.query.count ? parseInt(req.query.count as string) : 1;
 		const length = req.query.length
diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts
index 52a45c67..28293e59 100644
--- a/src/api/routes/auth/location-metadata.ts
+++ b/src/api/routes/auth/location-metadata.ts
@@ -16,20 +16,29 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { route } from "@spacebar/api";
-import { getIpAdress, IPAnalysis } from "@spacebar/api";
+import { IPAnalysis, getIpAdress, route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	//TODO
-	//Note: It's most likely related to legal. At the moment Discord hasn't finished this too
-	const country_code = (await IPAnalysis(getIpAdress(req))).country_code;
-	res.json({
-		consent_required: false,
-		country_code: country_code,
-		promotional_email_opt_in: { required: true, pre_checked: false },
-	});
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "LocationMetadataResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		//TODO
+		//Note: It's most likely related to legal. At the moment Discord hasn't finished this too
+		const country_code = (await IPAnalysis(getIpAdress(req))).country_code;
+		res.json({
+			consent_required: false,
+			country_code: country_code,
+			promotional_email_opt_in: { required: true, pre_checked: false },
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts
index fe0b4f99..d3fc1fb4 100644
--- a/src/api/routes/auth/login.ts
+++ b/src/api/routes/auth/login.ts
@@ -36,7 +36,17 @@ export default router;
 
 router.post(
 	"/",
-	route({ body: "LoginSchema" }),
+	route({
+		requestBody: "LoginSchema",
+		responses: {
+			200: {
+				body: "LoginResponse",
+			},
+			400: {
+				body: "APIErrorOrCaptchaResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { login, password, captcha_key, undelete } =
 			req.body as LoginSchema;
diff --git a/src/api/routes/auth/logout.ts b/src/api/routes/auth/logout.ts
index 51909afa..94a3e474 100644
--- a/src/api/routes/auth/logout.ts
+++ b/src/api/routes/auth/logout.ts
@@ -22,14 +22,25 @@ import { Request, Response, Router } from "express";
 const router: Router = Router();
 export default router;
 
-router.post("/", route({}), async (req: Request, res: Response) => {
-	if (req.body.provider != null || req.body.voip_provider != null) {
-		console.log(`[LOGOUT]: provider or voip provider not null!`, req.body);
-	} else {
-		delete req.body.provider;
-		delete req.body.voip_provider;
-		if (Object.keys(req.body).length != 0)
-			console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body);
-	}
-	res.status(204).send();
-});
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		if (req.body.provider != null || req.body.voip_provider != null) {
+			console.log(
+				`[LOGOUT]: provider or voip provider not null!`,
+				req.body,
+			);
+		} else {
+			delete req.body.provider;
+			delete req.body.voip_provider;
+			if (Object.keys(req.body).length != 0)
+				console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body);
+		}
+		res.status(204).send();
+	},
+);
diff --git a/src/api/routes/auth/mfa/totp.ts b/src/api/routes/auth/mfa/totp.ts
index 2396443d..4df408f9 100644
--- a/src/api/routes/auth/mfa/totp.ts
+++ b/src/api/routes/auth/mfa/totp.ts
@@ -16,16 +16,26 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
-import { BackupCode, generateToken, User, TotpSchema } from "@spacebar/util";
-import { verifyToken } from "node-2fa";
+import { BackupCode, TotpSchema, User, generateToken } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import { verifyToken } from "node-2fa";
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "TotpSchema" }),
+	route({
+		requestBody: "TotpSchema",
+		responses: {
+			200: {
+				body: "TokenResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		// const { code, ticket, gift_code_sku_id, login_source } =
 		const { code, ticket } = req.body as TotpSchema;
diff --git a/src/api/routes/auth/mfa/webauthn.ts b/src/api/routes/auth/mfa/webauthn.ts
index 1b387411..b58d2944 100644
--- a/src/api/routes/auth/mfa/webauthn.ts
+++ b/src/api/routes/auth/mfa/webauthn.ts
@@ -41,7 +41,13 @@ function toArrayBuffer(buf: Buffer) {
 
 router.post(
 	"/",
-	route({ body: "WebAuthnTotpSchema" }),
+	route({
+		requestBody: "WebAuthnTotpSchema",
+		responses: {
+			200: { body: "TokenResponse" },
+			400: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		if (!WebAuthn.fido2) {
 			// TODO: I did this for typescript and I can't use !
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index 430c9532..14dc319a 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -16,25 +16,25 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import {
+	IPAnalysis,
+	getIpAdress,
+	isProxy,
+	route,
+	verifyCaptcha,
+} from "@spacebar/api";
 import {
 	Config,
-	generateToken,
-	Invite,
 	FieldErrors,
-	User,
-	adjustEmail,
+	Invite,
 	RegisterSchema,
+	User,
 	ValidRegistrationToken,
+	adjustEmail,
+	generateToken,
 } from "@spacebar/util";
-import {
-	route,
-	getIpAdress,
-	IPAnalysis,
-	isProxy,
-	verifyCaptcha,
-} from "@spacebar/api";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { MoreThan } from "typeorm";
 
@@ -42,7 +42,13 @@ const router: Router = Router();
 
 router.post(
 	"/",
-	route({ body: "RegisterSchema" }),
+	route({
+		requestBody: "RegisterSchema",
+		responses: {
+			200: { body: "TokenOnlyResponse" },
+			400: { body: "APIErrorOrCaptchaResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as RegisterSchema;
 		const { register, security, limits } = Config.get();
@@ -219,6 +225,20 @@ router.post(
 		}
 
 		if (body.password) {
+			const min = register.password.minLength
+				? register.password.minLength
+				: 8;
+			if (body.password.length < min) {
+				throw FieldErrors({
+					password: {
+						code: "PASSWORD_REQUIREMENTS_MIN_LENGTH",
+						message: req.t(
+							"auth:register.PASSWORD_REQUIREMENTS_MIN_LENGTH",
+							{ min: min },
+						),
+					},
+				});
+			}
 			// the salt is saved in the password refer to bcrypt docs
 			body.password = await bcrypt.hash(body.password, 12);
 		} else if (register.password.required) {
diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts
index 852a43c7..f97045a6 100644
--- a/src/api/routes/auth/reset.ts
+++ b/src/api/routes/auth/reset.ts
@@ -31,9 +31,20 @@ import { Request, Response, Router } from "express";
 
 const router = Router();
 
+// TODO: the response interface also returns settings, but this route doesn't actually return that.
 router.post(
 	"/",
-	route({ body: "PasswordResetSchema" }),
+	route({
+		requestBody: "PasswordResetSchema",
+		responses: {
+			200: {
+				body: "TokenOnlyResponse",
+			},
+			400: {
+				body: "APIErrorOrCaptchaResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { password, token } = req.body as PasswordResetSchema;
 
diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts
index c1afcde9..a98c17fa 100644
--- a/src/api/routes/auth/verify/index.ts
+++ b/src/api/routes/auth/verify/index.ts
@@ -37,9 +37,20 @@ async function getToken(user: User) {
 	return { token };
 }
 
+// TODO: the response interface also returns settings, but this route doesn't actually return that.
 router.post(
 	"/",
-	route({ body: "VerifyEmailSchema" }),
+	route({
+		requestBody: "VerifyEmailSchema",
+		responses: {
+			200: {
+				body: "TokenResponse",
+			},
+			400: {
+				body: "APIErrorOrCaptchaResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { captcha_key, token } = req.body;
 
diff --git a/src/api/routes/auth/verify/resend.ts b/src/api/routes/auth/verify/resend.ts
index f2727abd..701f0ea8 100644
--- a/src/api/routes/auth/verify/resend.ts
+++ b/src/api/routes/auth/verify/resend.ts
@@ -24,7 +24,18 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ right: "RESEND_VERIFICATION_EMAIL" }),
+	route({
+		right: "RESEND_VERIFICATION_EMAIL",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			500: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const user = await User.findOneOrFail({
 			where: { id: req.user_id },
diff --git a/src/api/routes/auth/verify/view-backup-codes-challenge.ts b/src/api/routes/auth/verify/view-backup-codes-challenge.ts
index b12719ff..5407de82 100644
--- a/src/api/routes/auth/verify/view-backup-codes-challenge.ts
+++ b/src/api/routes/auth/verify/view-backup-codes-challenge.ts
@@ -16,15 +16,21 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
-import { FieldErrors, User, BackupCodesChallengeSchema } from "@spacebar/util";
+import { BackupCodesChallengeSchema, FieldErrors, User } from "@spacebar/util";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "BackupCodesChallengeSchema" }),
+	route({
+		requestBody: "BackupCodesChallengeSchema",
+		responses: {
+			200: { body: "BackupCodesChallengeResponse" },
+			400: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { password } = req.body as BackupCodesChallengeSchema;
 
diff --git a/src/api/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts
index db0d4242..567c7c92 100644
--- a/src/api/routes/channels/#channel_id/index.ts
+++ b/src/api/routes/channels/#channel_id/index.ts
@@ -16,18 +16,18 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelDeleteEvent,
+	ChannelModifySchema,
 	ChannelType,
 	ChannelUpdateEvent,
-	emitEvent,
 	Recipient,
+	emitEvent,
 	handleFile,
-	ChannelModifySchema,
 } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { route } from "@spacebar/api";
 
 const router: Router = Router();
 // TODO: delete channel
@@ -35,7 +35,15 @@ const router: Router = Router();
 
 router.get(
 	"/",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			200: {
+				body: "Channel",
+			},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 
@@ -49,7 +57,15 @@ router.get(
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_CHANNELS" }),
+	route({
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			200: {
+				body: "Channel",
+			},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 
@@ -90,7 +106,19 @@ router.delete(
 
 router.patch(
 	"/",
-	route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }),
+	route({
+		requestBody: "ChannelModifySchema",
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			200: {
+				body: "Channel",
+			},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const payload = req.body as ChannelModifySchema;
 		const { channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts
index 9f247fe8..b02f65d3 100644
--- a/src/api/routes/channels/#channel_id/invites.ts
+++ b/src/api/routes/channels/#channel_id/invites.ts
@@ -16,29 +16,37 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
-import { random } from "@spacebar/api";
+import { random, route } from "@spacebar/api";
 import {
 	Channel,
+	Guild,
 	Invite,
 	InviteCreateEvent,
-	emitEvent,
-	User,
-	Guild,
 	PublicInviteRelation,
+	User,
+	emitEvent,
+	isTextChannel,
 } from "@spacebar/util";
-import { isTextChannel } from "./messages";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
 
 const router: Router = Router();
 
 router.post(
 	"/",
 	route({
-		body: "InviteCreateSchema",
+		requestBody: "InviteCreateSchema",
 		permission: "CREATE_INSTANT_INVITE",
 		right: "CREATE_INVITES",
+		responses: {
+			201: {
+				body: "Invite",
+			},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { user_id } = req;
@@ -84,7 +92,15 @@ router.post(
 
 router.get(
 	"/",
-	route({ permission: "MANAGE_CHANNELS" }),
+	route({
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			200: {
+				body: "APIInviteArray",
+			},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 		const channel = await Channel.findOneOrFail({
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
index f098fa8e..a6dcae6b 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	emitEvent,
 	getPermission,
@@ -23,7 +24,6 @@ import {
 	ReadState,
 } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { route } from "@spacebar/api";
 
 const router = Router();
 
@@ -33,7 +33,13 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "MessageAcknowledgeSchema" }),
+	route({
+		requestBody: "MessageAcknowledgeSchema",
+		responses: {
+			200: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
 
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
index 909a459e..5ca645c0 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
@@ -16,14 +16,21 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.post(
 	"/",
-	route({ permission: "MANAGE_MESSAGES" }),
+	route({
+		permission: "MANAGE_MESSAGES",
+		responses: {
+			200: {
+				body: "Message",
+			},
+		},
+	}),
 	(req: Request, res: Response) => {
 		// TODO:
 		res.json({
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
index cd4b243e..6bc03f53 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -19,24 +19,23 @@
 import {
 	Attachment,
 	Channel,
-	emitEvent,
-	SpacebarApiErrors,
-	getPermission,
-	getRights,
 	Message,
 	MessageCreateEvent,
+	MessageCreateSchema,
 	MessageDeleteEvent,
+	MessageEditSchema,
 	MessageUpdateEvent,
 	Snowflake,
+	SpacebarApiErrors,
+	emitEvent,
+	getPermission,
+	getRights,
 	uploadFile,
-	MessageCreateSchema,
-	MessageEditSchema,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
-import multer from "multer";
-import { route } from "@spacebar/api";
-import { handleMessage, postHandleMessage } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import multer from "multer";
+import { handleMessage, postHandleMessage, route } from "../../../../../util";
 
 const router = Router();
 // TODO: message content/embed string length limit
@@ -53,9 +52,19 @@ const messageUpload = multer({
 router.patch(
 	"/",
 	route({
-		body: "MessageEditSchema",
+		requestBody: "MessageEditSchema",
 		permission: "SEND_MESSAGES",
 		right: "SEND_MESSAGES",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
@@ -143,9 +152,19 @@ router.put(
 		next();
 	},
 	route({
-		body: "MessageCreateSchema",
+		requestBody: "MessageCreateSchema",
 		permission: "SEND_MESSAGES",
 		right: "SEND_BACKDATED_EVENTS",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
@@ -230,7 +249,19 @@ router.put(
 
 router.get(
 	"/",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 
@@ -252,38 +283,54 @@ router.get(
 	},
 );
 
-router.delete("/", route({}), async (req: Request, res: Response) => {
-	const { message_id, channel_id } = req.params;
+router.delete(
+	"/",
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { message_id, channel_id } = req.params;
 
-	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
-	const message = await Message.findOneOrFail({ where: { id: message_id } });
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+		});
+		const message = await Message.findOneOrFail({
+			where: { id: message_id },
+		});
 
-	const rights = await getRights(req.user_id);
+		const rights = await getRights(req.user_id);
 
-	if (message.author_id !== req.user_id) {
-		if (!rights.has("MANAGE_MESSAGES")) {
-			const permission = await getPermission(
-				req.user_id,
-				channel.guild_id,
-				channel_id,
-			);
-			permission.hasThrow("MANAGE_MESSAGES");
-		}
-	} else rights.hasThrow("SELF_DELETE_MESSAGES");
+		if (message.author_id !== req.user_id) {
+			if (!rights.has("MANAGE_MESSAGES")) {
+				const permission = await getPermission(
+					req.user_id,
+					channel.guild_id,
+					channel_id,
+				);
+				permission.hasThrow("MANAGE_MESSAGES");
+			}
+		} else rights.hasThrow("SELF_DELETE_MESSAGES");
 
-	await Message.delete({ id: message_id });
+		await Message.delete({ id: message_id });
 
-	await emitEvent({
-		event: "MESSAGE_DELETE",
-		channel_id,
-		data: {
-			id: message_id,
+		await emitEvent({
+			event: "MESSAGE_DELETE",
 			channel_id,
-			guild_id: channel.guild_id,
-		},
-	} as MessageDeleteEvent);
+			data: {
+				id: message_id,
+				channel_id,
+				guild_id: channel.guild_id,
+			},
+		} as MessageDeleteEvent);
 
-	res.sendStatus(204);
-});
+		res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
index cb66cd64..5efa0f14 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	emitEvent,
@@ -32,8 +33,7 @@ import {
 	PublicUserProjection,
 	User,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { In } from "typeorm";
 
@@ -57,7 +57,17 @@ function getEmoji(emoji: string): PartialEmoji {
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_MESSAGES" }),
+	route({
+		permission: "MANAGE_MESSAGES",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 
@@ -83,7 +93,17 @@ router.delete(
 
 router.delete(
 	"/:emoji",
-	route({ permission: "MANAGE_MESSAGES" }),
+	route({
+		permission: "MANAGE_MESSAGES",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 		const emoji = getEmoji(req.params.emoji);
@@ -120,7 +140,19 @@ router.delete(
 
 router.get(
 	"/:emoji",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			200: {
+				body: "PublicUser",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 		const emoji = getEmoji(req.params.emoji);
@@ -148,7 +180,18 @@ router.get(
 
 router.put(
 	"/:emoji/:user_id",
-	route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }),
+	route({
+		permission: "READ_MESSAGE_HISTORY",
+		right: "SELF_ADD_REACTIONS",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id, user_id } = req.params;
 		if (user_id !== "@me") throw new HTTPError("Invalid user");
@@ -219,7 +262,16 @@ router.put(
 
 router.delete(
 	"/:emoji/:user_id",
-	route({}),
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		let { user_id } = req.params;
 		const { message_id, channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
index 18476d5c..9b607d59 100644
--- a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
+++ b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -16,18 +16,18 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	Config,
 	emitEvent,
 	getPermission,
 	getRights,
-	MessageDeleteBulkEvent,
 	Message,
+	MessageDeleteBulkEvent,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 
 const router: Router = Router();
 
@@ -38,7 +38,17 @@ export default router;
 // https://discord.com/developers/docs/resources/channel#bulk-delete-messages
 router.post(
 	"/",
-	route({ body: "BulkDeleteSchema" }),
+	route({
+		requestBody: "BulkDeleteSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 		const channel = await Channel.findOneOrFail({
diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts
index 7f0c9fb5..a15b462d 100644
--- a/src/api/routes/channels/#channel_id/messages/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -16,165 +16,172 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { handleMessage, postHandleMessage, route } from "@spacebar/api";
 import {
 	Attachment,
 	Channel,
-	ChannelType,
 	Config,
 	DmChannelDTO,
-	emitEvent,
 	FieldErrors,
-	getPermission,
+	Member,
 	Message,
 	MessageCreateEvent,
-	Snowflake,
-	uploadFile,
-	Member,
 	MessageCreateSchema,
+	Reaction,
 	ReadState,
 	Rights,
-	Reaction,
+	Snowflake,
 	User,
+	emitEvent,
+	getPermission,
+	isTextChannel,
+	uploadFile,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { handleMessage, postHandleMessage, route } from "@spacebar/api";
 import multer from "multer";
 import { FindManyOptions, FindOperator, LessThan, MoreThan } from "typeorm";
 import { URL } from "url";
 
 const router: Router = Router();
 
-export default router;
-
-export function isTextChannel(type: ChannelType): boolean {
-	switch (type) {
-		case ChannelType.GUILD_STORE:
-		case ChannelType.GUILD_VOICE:
-		case ChannelType.GUILD_STAGE_VOICE:
-		case ChannelType.GUILD_CATEGORY:
-		case ChannelType.GUILD_FORUM:
-		case ChannelType.DIRECTORY:
-			throw new HTTPError("not a text channel", 400);
-		case ChannelType.DM:
-		case ChannelType.GROUP_DM:
-		case ChannelType.GUILD_NEWS:
-		case ChannelType.GUILD_NEWS_THREAD:
-		case ChannelType.GUILD_PUBLIC_THREAD:
-		case ChannelType.GUILD_PRIVATE_THREAD:
-		case ChannelType.GUILD_TEXT:
-		case ChannelType.ENCRYPTED:
-		case ChannelType.ENCRYPTED_THREAD:
-			return true;
-		default:
-			throw new HTTPError("unimplemented", 400);
-	}
-}
-
 // https://discord.com/developers/docs/resources/channel#create-message
 // get messages
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const channel_id = req.params.channel_id;
-	const channel = await Channel.findOneOrFail({
-		where: { id: channel_id },
-	});
-	if (!channel) throw new HTTPError("Channel not found", 404);
-
-	isTextChannel(channel.type);
-	const around = req.query.around ? `${req.query.around}` : undefined;
-	const before = req.query.before ? `${req.query.before}` : undefined;
-	const after = req.query.after ? `${req.query.after}` : undefined;
-	const limit = Number(req.query.limit) || 50;
-	if (limit < 1 || limit > 100)
-		throw new HTTPError("limit must be between 1 and 100", 422);
-
-	const halfLimit = Math.floor(limit / 2);
-
-	const permissions = await getPermission(
-		req.user_id,
-		channel.guild_id,
-		channel_id,
-	);
-	permissions.hasThrow("VIEW_CHANNEL");
-	if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
-
-	const query: FindManyOptions<Message> & {
-		where: { id?: FindOperator<string> | FindOperator<string>[] };
-	} = {
-		order: { timestamp: "DESC" },
-		take: limit,
-		where: { channel_id },
-		relations: [
-			"author",
-			"webhook",
-			"application",
-			"mentions",
-			"mention_roles",
-			"mention_channels",
-			"sticker_items",
-			"attachments",
-		],
-	};
-
-	if (after) {
-		if (BigInt(after) > BigInt(Snowflake.generate()))
-			return res.status(422);
-		query.where.id = MoreThan(after);
-	} else if (before) {
-		if (BigInt(before) < BigInt(req.params.channel_id))
-			return res.status(422);
-		query.where.id = LessThan(before);
-	} else if (around) {
-		query.where.id = [
-			MoreThan((BigInt(around) - BigInt(halfLimit)).toString()),
-			LessThan((BigInt(around) + BigInt(halfLimit)).toString()),
-		];
-
-		return res.json([]); // TODO: fix around
-	}
+router.get(
+	"/",
+	route({
+		query: {
+			around: {
+				type: "string",
+			},
+			before: {
+				type: "string",
+			},
+			after: {
+				type: "string",
+			},
+			limit: {
+				type: "number",
+				description:
+					"max number of messages to return (1-100). defaults to 50",
+			},
+		},
+		responses: {
+			200: {
+				body: "APIMessageArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const channel_id = req.params.channel_id;
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+		});
+		if (!channel) throw new HTTPError("Channel not found", 404);
+
+		isTextChannel(channel.type);
+		const around = req.query.around ? `${req.query.around}` : undefined;
+		const before = req.query.before ? `${req.query.before}` : undefined;
+		const after = req.query.after ? `${req.query.after}` : undefined;
+		const limit = Number(req.query.limit) || 50;
+		if (limit < 1 || limit > 100)
+			throw new HTTPError("limit must be between 1 and 100", 422);
+
+		const permissions = await getPermission(
+			req.user_id,
+			channel.guild_id,
+			channel_id,
+		);
+		permissions.hasThrow("VIEW_CHANNEL");
+		if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
+
+		const query: FindManyOptions<Message> & {
+			where: { id?: FindOperator<string> | FindOperator<string>[] };
+		} = {
+			order: { timestamp: "DESC" },
+			take: limit,
+			where: { channel_id },
+			relations: [
+				"author",
+				"webhook",
+				"application",
+				"mentions",
+				"mention_roles",
+				"mention_channels",
+				"sticker_items",
+				"attachments",
+			],
+		};
+
+		let messages: Message[];
+		if (after) {
+			if (BigInt(after) > BigInt(Snowflake.generate()))
+				return res.status(422);
+			query.where.id = MoreThan(after);
+			messages = await Message.find(query);
+		} else if (before) {
+			if (BigInt(before) < BigInt(req.params.channel_id))
+				return res.status(422);
+			query.where.id = LessThan(before);
+			messages = await Message.find(query);
+		} else if (around) {
+			query.take = Math.floor(limit / 2);
+			query.where.id = LessThan(around);
+			const messages_before = await Message.find(query);
+			query.where.id = MoreThan(around);
+			const messages_after = await Message.find(query);
+			messages = messages_before.concat(messages_after);
+		} else {
+			throw new HTTPError("after, around or before must be present", 422);
+		}
 
-	const messages = await Message.find(query);
-	const endpoint = Config.get().cdn.endpointPublic;
+		const endpoint = Config.get().cdn.endpointPublic;
 
-	return res.json(
-		messages.map((x: Partial<Message>) => {
-			(x.reactions || []).forEach((y: Partial<Reaction>) => {
-				// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-				//@ts-ignore
-				if ((y.user_ids || []).includes(req.user_id)) y.me = true;
-				delete y.user_ids;
-			});
-			if (!x.author)
-				x.author = User.create({
-					id: "4",
-					discriminator: "0000",
-					username: "Spacebar Ghost",
-					public_flags: 0,
+		return res.json(
+			messages.map((x: Partial<Message>) => {
+				(x.reactions || []).forEach((y: Partial<Reaction>) => {
+					// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+					//@ts-ignore
+					if ((y.user_ids || []).includes(req.user_id)) y.me = true;
+					delete y.user_ids;
+				});
+				if (!x.author)
+					x.author = User.create({
+						id: "4",
+						discriminator: "0000",
+						username: "Spacebar Ghost",
+						public_flags: 0,
+					});
+				x.attachments?.forEach((y: Attachment) => {
+					// dynamically set attachment proxy_url in case the endpoint changed
+					const uri = y.proxy_url.startsWith("http")
+						? y.proxy_url
+						: `https://example.org${y.proxy_url}`;
+					y.proxy_url = `${endpoint == null ? "" : endpoint}${
+						new URL(uri).pathname
+					}`;
 				});
-			x.attachments?.forEach((y: Attachment) => {
-				// dynamically set attachment proxy_url in case the endpoint changed
-				const uri = y.proxy_url.startsWith("http")
-					? y.proxy_url
-					: `https://example.org${y.proxy_url}`;
-				y.proxy_url = `${endpoint == null ? "" : endpoint}${
-					new URL(uri).pathname
-				}`;
-			});
 
-			/**
+				/**
 			Some clients ( discord.js ) only check if a property exists within the response,
 			which causes errors when, say, the `application` property is `null`.
 			**/
 
-			// for (var curr in x) {
-			// 	if (x[curr] === null)
-			// 		delete x[curr];
-			// }
+				// for (var curr in x) {
+				// 	if (x[curr] === null)
+				// 		delete x[curr];
+				// }
 
-			return x;
-		}),
-	);
-});
+				return x;
+			}),
+		);
+	},
+);
 
 // TODO: config max upload size
 const messageUpload = multer({
@@ -205,9 +212,19 @@ router.post(
 		next();
 	},
 	route({
-		body: "MessageCreateSchema",
+		requestBody: "MessageCreateSchema",
 		permission: "SEND_MESSAGES",
 		right: "SEND_MESSAGES",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
@@ -366,3 +383,5 @@ router.post(
 		return res.json(message);
 	},
 );
+
+export default router;
diff --git a/src/api/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts
index 68dbc2f2..d3edb0fa 100644
--- a/src/api/routes/channels/#channel_id/permissions.ts
+++ b/src/api/routes/channels/#channel_id/permissions.ts
@@ -19,13 +19,13 @@
 import {
 	Channel,
 	ChannelPermissionOverwrite,
+	ChannelPermissionOverwriteSchema,
 	ChannelUpdateEvent,
 	emitEvent,
 	Member,
 	Role,
-	ChannelPermissionOverwriteSchema,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 import { route } from "@spacebar/api";
@@ -36,8 +36,14 @@ const router: Router = Router();
 router.put(
 	"/:overwrite_id",
 	route({
-		body: "ChannelPermissionOverwriteSchema",
+		requestBody: "ChannelPermissionOverwriteSchema",
 		permission: "MANAGE_ROLES",
+		responses: {
+			204: {},
+			404: {},
+			501: {},
+			400: { body: "APIErrorResponse" },
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, overwrite_id } = req.params;
@@ -92,7 +98,7 @@ router.put(
 // TODO: check permission hierarchy
 router.delete(
 	"/:overwrite_id",
-	route({ permission: "MANAGE_ROLES" }),
+	route({ permission: "MANAGE_ROLES", responses: { 204: {}, 404: {} } }),
 	async (req: Request, res: Response) => {
 		const { channel_id, overwrite_id } = req.params;
 
diff --git a/src/api/routes/channels/#channel_id/pins.ts b/src/api/routes/channels/#channel_id/pins.ts
index 32820916..724ebffd 100644
--- a/src/api/routes/channels/#channel_id/pins.ts
+++ b/src/api/routes/channels/#channel_id/pins.ts
@@ -16,23 +16,33 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelPinsUpdateEvent,
 	Config,
+	DiscordApiErrors,
 	emitEvent,
 	Message,
 	MessageUpdateEvent,
-	DiscordApiErrors,
 } from "@spacebar/util";
-import { Router, Request, Response } from "express";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.put(
 	"/:message_id",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			204: {},
+			403: {},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
 
@@ -74,7 +84,17 @@ router.put(
 
 router.delete(
 	"/:message_id",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			204: {},
+			403: {},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
 
@@ -114,7 +134,17 @@ router.delete(
 
 router.get(
 	"/",
-	route({ permission: ["READ_MESSAGE_HISTORY"] }),
+	route({
+		permission: ["READ_MESSAGE_HISTORY"],
+		responses: {
+			200: {
+				body: "APIMessageArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 
diff --git a/src/api/routes/channels/#channel_id/purge.ts b/src/api/routes/channels/#channel_id/purge.ts
index c8da6760..012fec1c 100644
--- a/src/api/routes/channels/#channel_id/purge.ts
+++ b/src/api/routes/channels/#channel_id/purge.ts
@@ -16,20 +16,20 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { HTTPError } from "lambert-server";
 import { route } from "@spacebar/api";
-import { isTextChannel } from "./messages";
-import { FindManyOptions, Between, Not, FindOperator } from "typeorm";
 import {
 	Channel,
-	emitEvent,
-	getPermission,
-	getRights,
 	Message,
 	MessageDeleteBulkEvent,
 	PurgeSchema,
+	emitEvent,
+	getPermission,
+	getRights,
+	isTextChannel,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+import { Between, FindManyOptions, FindOperator, Not } from "typeorm";
 
 const router: Router = Router();
 
@@ -42,6 +42,14 @@ router.post(
 	"/",
 	route({
 		/*body: "PurgeSchema",*/
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts
index f1fb48af..569bb5cd 100644
--- a/src/api/routes/channels/#channel_id/recipients.ts
+++ b/src/api/routes/channels/#channel_id/recipients.ts
@@ -16,7 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelRecipientAddEvent,
@@ -28,80 +28,98 @@ import {
 	Recipient,
 	User,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.put("/:user_id", route({}), async (req: Request, res: Response) => {
-	const { channel_id, user_id } = req.params;
-	const channel = await Channel.findOneOrFail({
-		where: { id: channel_id },
-		relations: ["recipients"],
-	});
+router.put(
+	"/:user_id",
+	route({
+		responses: {
+			201: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { channel_id, user_id } = req.params;
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+			relations: ["recipients"],
+		});
 
-	if (channel.type !== ChannelType.GROUP_DM) {
-		const recipients = [
-			...(channel.recipients?.map((r) => r.user_id) || []),
-			user_id,
-		].unique();
+		if (channel.type !== ChannelType.GROUP_DM) {
+			const recipients = [
+				...(channel.recipients?.map((r) => r.user_id) || []),
+				user_id,
+			].unique();
 
-		const new_channel = await Channel.createDMChannel(
-			recipients,
-			req.user_id,
-		);
-		return res.status(201).json(new_channel);
-	} else {
-		if (channel.recipients?.map((r) => r.user_id).includes(user_id)) {
-			throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
-		}
+			const new_channel = await Channel.createDMChannel(
+				recipients,
+				req.user_id,
+			);
+			return res.status(201).json(new_channel);
+		} else {
+			if (channel.recipients?.map((r) => r.user_id).includes(user_id)) {
+				throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
+			}
 
-		channel.recipients?.push(
-			Recipient.create({ channel_id: channel_id, user_id: user_id }),
-		);
-		await channel.save();
+			channel.recipients?.push(
+				Recipient.create({ channel_id: channel_id, user_id: user_id }),
+			);
+			await channel.save();
 
-		await emitEvent({
-			event: "CHANNEL_CREATE",
-			data: await DmChannelDTO.from(channel, [user_id]),
-			user_id: user_id,
-		});
+			await emitEvent({
+				event: "CHANNEL_CREATE",
+				data: await DmChannelDTO.from(channel, [user_id]),
+				user_id: user_id,
+			});
 
-		await emitEvent({
-			event: "CHANNEL_RECIPIENT_ADD",
-			data: {
+			await emitEvent({
+				event: "CHANNEL_RECIPIENT_ADD",
+				data: {
+					channel_id: channel_id,
+					user: await User.findOneOrFail({
+						where: { id: user_id },
+						select: PublicUserProjection,
+					}),
+				},
 				channel_id: channel_id,
-				user: await User.findOneOrFail({
-					where: { id: user_id },
-					select: PublicUserProjection,
-				}),
-			},
-			channel_id: channel_id,
-		} as ChannelRecipientAddEvent);
-		return res.sendStatus(204);
-	}
-});
+			} as ChannelRecipientAddEvent);
+			return res.sendStatus(204);
+		}
+	},
+);
 
-router.delete("/:user_id", route({}), async (req: Request, res: Response) => {
-	const { channel_id, user_id } = req.params;
-	const channel = await Channel.findOneOrFail({
-		where: { id: channel_id },
-		relations: ["recipients"],
-	});
-	if (
-		!(
-			channel.type === ChannelType.GROUP_DM &&
-			(channel.owner_id === req.user_id || user_id === req.user_id)
+router.delete(
+	"/:user_id",
+	route({
+		responses: {
+			204: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { channel_id, user_id } = req.params;
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+			relations: ["recipients"],
+		});
+		if (
+			!(
+				channel.type === ChannelType.GROUP_DM &&
+				(channel.owner_id === req.user_id || user_id === req.user_id)
+			)
 		)
-	)
-		throw DiscordApiErrors.MISSING_PERMISSIONS;
+			throw DiscordApiErrors.MISSING_PERMISSIONS;
 
-	if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) {
-		throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
-	}
+		if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) {
+			throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
+		}
 
-	await Channel.removeRecipientFromChannel(channel, user_id);
+		await Channel.removeRecipientFromChannel(channel, user_id);
 
-	return res.sendStatus(204);
-});
+		return res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/channels/#channel_id/typing.ts b/src/api/routes/channels/#channel_id/typing.ts
index 6a2fef39..b5d61d74 100644
--- a/src/api/routes/channels/#channel_id/typing.ts
+++ b/src/api/routes/channels/#channel_id/typing.ts
@@ -16,15 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util";
 import { route } from "@spacebar/api";
-import { Router, Request, Response } from "express";
+import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.post(
 	"/",
-	route({ permission: "SEND_MESSAGES" }),
+	route({
+		permission: "SEND_MESSAGES",
+		responses: {
+			204: {},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 		const user_id = req.user_id;
diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts
index 14791a1c..d54756a1 100644
--- a/src/api/routes/channels/#channel_id/webhooks.ts
+++ b/src/api/routes/channels/#channel_id/webhooks.ts
@@ -16,34 +16,56 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
 import {
 	Channel,
 	Config,
-	handleFile,
-	trimSpecial,
+	DiscordApiErrors,
 	User,
 	Webhook,
 	WebhookCreateSchema,
 	WebhookType,
+	handleFile,
+	trimSpecial,
+	isTextChannel,
 } from "@spacebar/util";
-import { HTTPError } from "lambert-server";
-import { isTextChannel } from "./messages/index";
-import { DiscordApiErrors } from "@spacebar/util";
 import crypto from "crypto";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
 
 const router: Router = Router();
 
 //TODO: implement webhooks
-router.get("/", route({}), async (req: Request, res: Response) => {
-	res.json([]);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIWebhookArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		res.json([]);
+	},
+);
 
 // TODO: use Image Data Type for avatar instead of String
 router.post(
 	"/",
-	route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }),
+	route({
+		requestBody: "WebhookCreateSchema",
+		permission: "MANAGE_WEBHOOKS",
+		responses: {
+			200: {
+				body: "WebhookCreateResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const channel_id = req.params.channel_id;
 		const channel = await Channel.findOneOrFail({
diff --git a/src/api/routes/connections/#connection_name/callback.ts b/src/api/routes/connections/#connection_name/callback.ts
index bc9ba455..ee0db94a 100644
--- a/src/api/routes/connections/#connection_name/callback.ts
+++ b/src/api/routes/connections/#connection_name/callback.ts
@@ -29,7 +29,7 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "ConnectionCallbackSchema" }),
+	route({ requestBody: "ConnectionCallbackSchema" }),
 	async (req: Request, res: Response) => {
 		const { connection_name } = req.params;
 		const connection = ConnectionStore.connections.get(connection_name);
diff --git a/src/api/routes/discoverable-guilds.ts b/src/api/routes/discoverable-guilds.ts
index 75eb6088..b8c6a386 100644
--- a/src/api/routes/discoverable-guilds.ts
+++ b/src/api/routes/discoverable-guilds.ts
@@ -16,49 +16,61 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Guild, Config } from "@spacebar/util";
+import { Config, Guild } from "@spacebar/util";
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { Like } from "typeorm";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { offset, limit, categories } = req.query;
-	const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
-	const configLimit = Config.get().guild.discovery.limit;
-	let guilds;
-	if (categories == undefined) {
-		guilds = showAllGuilds
-			? await Guild.find({ take: Math.abs(Number(limit || configLimit)) })
-			: await Guild.find({
-					where: { features: Like(`%DISCOVERABLE%`) },
-					take: Math.abs(Number(limit || configLimit)),
-			  });
-	} else {
-		guilds = showAllGuilds
-			? await Guild.find({
-					where: { primary_category_id: categories.toString() },
-					take: Math.abs(Number(limit || configLimit)),
-			  })
-			: await Guild.find({
-					where: {
-						primary_category_id: categories.toString(),
-						features: Like("%DISCOVERABLE%"),
-					},
-					take: Math.abs(Number(limit || configLimit)),
-			  });
-	}
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "DiscoverableGuildsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { offset, limit, categories } = req.query;
+		const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
+		const configLimit = Config.get().guild.discovery.limit;
+		let guilds;
+		if (categories == undefined) {
+			guilds = showAllGuilds
+				? await Guild.find({
+						take: Math.abs(Number(limit || configLimit)),
+				  })
+				: await Guild.find({
+						where: { features: Like(`%DISCOVERABLE%`) },
+						take: Math.abs(Number(limit || configLimit)),
+				  });
+		} else {
+			guilds = showAllGuilds
+				? await Guild.find({
+						where: { primary_category_id: categories.toString() },
+						take: Math.abs(Number(limit || configLimit)),
+				  })
+				: await Guild.find({
+						where: {
+							primary_category_id: categories.toString(),
+							features: Like("%DISCOVERABLE%"),
+						},
+						take: Math.abs(Number(limit || configLimit)),
+				  });
+		}
 
-	const total = guilds ? guilds.length : undefined;
+		const total = guilds ? guilds.length : undefined;
 
-	res.send({
-		total: total,
-		guilds: guilds,
-		offset: Number(offset || Config.get().guild.discovery.offset),
-		limit: Number(limit || configLimit),
-	});
-});
+		res.send({
+			total: total,
+			guilds: guilds,
+			offset: Number(offset || Config.get().guild.discovery.offset),
+			limit: Number(limit || configLimit),
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/discovery.ts b/src/api/routes/discovery.ts
index 0c8089e4..a045c191 100644
--- a/src/api/routes/discovery.ts
+++ b/src/api/routes/discovery.ts
@@ -16,24 +16,34 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Categories } from "@spacebar/util";
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { Categories } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/categories", route({}), async (req: Request, res: Response) => {
-	// TODO:
-	// Get locale instead
+router.get(
+	"/categories",
+	route({
+		responses: {
+			200: {
+				body: "APIDiscoveryCategoryArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO:
+		// Get locale instead
 
-	// const { locale, primary_only } = req.query;
-	const { primary_only } = req.query;
+		// const { locale, primary_only } = req.query;
+		const { primary_only } = req.query;
 
-	const out = primary_only
-		? await Categories.find()
-		: await Categories.find({ where: { is_primary: true } });
+		const out = primary_only
+			? await Categories.find()
+			: await Categories.find({ where: { is_primary: true } });
 
-	res.send(out);
-});
+		res.send(out);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/download.ts b/src/api/routes/download.ts
index c4eea8e8..85fb41be 100644
--- a/src/api/routes/download.ts
+++ b/src/api/routes/download.ts
@@ -16,32 +16,43 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
 import { FieldErrors, Release } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { platform } = req.query;
+router.get(
+	"/",
+	route({
+		responses: {
+			302: {},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { platform } = req.query;
+
+		if (!platform)
+			throw FieldErrors({
+				platform: {
+					code: "BASE_TYPE_REQUIRED",
+					message: req.t("common:field.BASE_TYPE_REQUIRED"),
+				},
+			});
 
-	if (!platform)
-		throw FieldErrors({
-			platform: {
-				code: "BASE_TYPE_REQUIRED",
-				message: req.t("common:field.BASE_TYPE_REQUIRED"),
+		const release = await Release.findOneOrFail({
+			where: {
+				enabled: true,
+				platform: platform as string,
 			},
+			order: { pub_date: "DESC" },
 		});
 
-	const release = await Release.findOneOrFail({
-		where: {
-			enabled: true,
-			platform: platform as string,
-		},
-		order: { pub_date: "DESC" },
-	});
-
-	res.redirect(release.url);
-});
+		res.redirect(release.url);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/gateway/bot.ts b/src/api/routes/gateway/bot.ts
index 243159ec..d9101159 100644
--- a/src/api/routes/gateway/bot.ts
+++ b/src/api/routes/gateway/bot.ts
@@ -16,32 +16,34 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
-import { Router, Response, Request } from "express";
-import { route, RouteOptions } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-const options: RouteOptions = {
-	test: {
-		response: {
-			body: "GatewayBotResponse",
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GatewayBotResponse",
+			},
 		},
+	}),
+	(req: Request, res: Response) => {
+		const { endpointPublic } = Config.get().gateway;
+		res.json({
+			url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
+			shards: 1,
+			session_start_limit: {
+				total: 1000,
+				remaining: 999,
+				reset_after: 14400000,
+				max_concurrency: 1,
+			},
+		});
 	},
-};
-
-router.get("/", route(options), (req: Request, res: Response) => {
-	const { endpointPublic } = Config.get().gateway;
-	res.json({
-		url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
-		shards: 1,
-		session_start_limit: {
-			total: 1000,
-			remaining: 999,
-			reset_after: 14400000,
-			max_concurrency: 1,
-		},
-	});
-});
+);
 
 export default router;
diff --git a/src/api/routes/gateway/index.ts b/src/api/routes/gateway/index.ts
index 12e96919..9100d5ee 100644
--- a/src/api/routes/gateway/index.ts
+++ b/src/api/routes/gateway/index.ts
@@ -16,25 +16,27 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
-import { Router, Response, Request } from "express";
-import { route, RouteOptions } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-const options: RouteOptions = {
-	test: {
-		response: {
-			body: "GatewayResponse",
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GatewayResponse",
+			},
 		},
+	}),
+	(req: Request, res: Response) => {
+		const { endpointPublic } = Config.get().gateway;
+		res.json({
+			url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
+		});
 	},
-};
-
-router.get("/", route(options), (req: Request, res: Response) => {
-	const { endpointPublic } = Config.get().gateway;
-	res.json({
-		url: endpointPublic || process.env.GATEWAY || "ws://localhost:3001",
-	});
-});
+);
 
 export default router;
diff --git a/src/api/routes/gifs/search.ts b/src/api/routes/gifs/search.ts
index fb99374b..f125a463 100644
--- a/src/api/routes/gifs/search.ts
+++ b/src/api/routes/gifs/search.ts
@@ -16,34 +16,62 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
+import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import fetch from "node-fetch";
 import ProxyAgent from "proxy-agent";
-import { route } from "@spacebar/api";
-import { getGifApiKey, parseGifResult } from "./trending";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// TODO: Custom providers
-	const { q, media_format, locale } = req.query;
+router.get(
+	"/",
+	route({
+		query: {
+			q: {
+				type: "string",
+				required: true,
+				description: "Search query",
+			},
+			media_format: {
+				type: "string",
+				description: "Media format",
+				values: Object.keys(TenorMediaTypes).filter((key) =>
+					isNaN(Number(key)),
+				),
+			},
+			locale: {
+				type: "string",
+				description: "Locale",
+			},
+		},
+		responses: {
+			200: {
+				body: "TenorGifsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: Custom providers
+		const { q, media_format, locale } = req.query;
 
-	const apiKey = getGifApiKey();
+		const apiKey = getGifApiKey();
 
-	const agent = new ProxyAgent();
+		const agent = new ProxyAgent();
 
-	const response = await fetch(
-		`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`,
-		{
-			agent,
-			method: "get",
-			headers: { "Content-Type": "application/json" },
-		},
-	);
+		const response = await fetch(
+			`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`,
+			{
+				agent,
+				method: "get",
+				headers: { "Content-Type": "application/json" },
+			},
+		);
 
-	const { results } = await response.json();
+		const { results } = await response.json();
 
-	res.json(results.map(parseGifResult)).status(200);
-});
+		res.json(results.map(parseGifResult)).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/gifs/trending-gifs.ts b/src/api/routes/gifs/trending-gifs.ts
index 238a2abd..d6fa89ac 100644
--- a/src/api/routes/gifs/trending-gifs.ts
+++ b/src/api/routes/gifs/trending-gifs.ts
@@ -16,34 +16,57 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
+import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import fetch from "node-fetch";
 import ProxyAgent from "proxy-agent";
-import { route } from "@spacebar/api";
-import { getGifApiKey, parseGifResult } from "./trending";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// TODO: Custom providers
-	const { media_format, locale } = req.query;
+router.get(
+	"/",
+	route({
+		query: {
+			media_format: {
+				type: "string",
+				description: "Media format",
+				values: Object.keys(TenorMediaTypes).filter((key) =>
+					isNaN(Number(key)),
+				),
+			},
+			locale: {
+				type: "string",
+				description: "Locale",
+			},
+		},
+		responses: {
+			200: {
+				body: "TenorGifsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: Custom providers
+		const { media_format, locale } = req.query;
 
-	const apiKey = getGifApiKey();
+		const apiKey = getGifApiKey();
 
-	const agent = new ProxyAgent();
+		const agent = new ProxyAgent();
 
-	const response = await fetch(
-		`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`,
-		{
-			agent,
-			method: "get",
-			headers: { "Content-Type": "application/json" },
-		},
-	);
+		const response = await fetch(
+			`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`,
+			{
+				agent,
+				method: "get",
+				headers: { "Content-Type": "application/json" },
+			},
+		);
 
-	const { results } = await response.json();
+		const { results } = await response.json();
 
-	res.json(results.map(parseGifResult)).status(200);
-});
+		res.json(results.map(parseGifResult)).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/gifs/trending.ts b/src/api/routes/gifs/trending.ts
index 5cccdb2d..e3d6e974 100644
--- a/src/api/routes/gifs/trending.ts
+++ b/src/api/routes/gifs/trending.ts
@@ -16,126 +16,76 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
+import {
+	TenorCategoriesResults,
+	TenorTrendingResults,
+	getGifApiKey,
+	parseGifResult,
+} from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import fetch from "node-fetch";
 import ProxyAgent from "proxy-agent";
-import { route } from "@spacebar/api";
-import { Config } from "@spacebar/util";
-import { HTTPError } from "lambert-server";
 
 const router = Router();
 
-// TODO: Move somewhere else
-enum TENOR_GIF_TYPES {
-	gif,
-	mediumgif,
-	tinygif,
-	nanogif,
-	mp4,
-	loopedmp4,
-	tinymp4,
-	nanomp4,
-	webm,
-	tinywebm,
-	nanowebm,
-}
-
-type TENOR_MEDIA = {
-	preview: string;
-	url: string;
-	dims: number[];
-	size: number;
-};
-
-type TENOR_GIF = {
-	created: number;
-	hasaudio: boolean;
-	id: string;
-	media: { [type in keyof typeof TENOR_GIF_TYPES]: TENOR_MEDIA }[];
-	tags: string[];
-	title: string;
-	itemurl: string;
-	hascaption: boolean;
-	url: string;
-};
-
-type TENOR_CATEGORY = {
-	searchterm: string;
-	path: string;
-	image: string;
-	name: string;
-};
-
-type TENOR_CATEGORIES_RESULTS = {
-	tags: TENOR_CATEGORY[];
-};
-
-type TENOR_TRENDING_RESULTS = {
-	next: string;
-	results: TENOR_GIF[];
-};
-
-export function parseGifResult(result: TENOR_GIF) {
-	return {
-		id: result.id,
-		title: result.title,
-		url: result.itemurl,
-		src: result.media[0].mp4.url,
-		gif_src: result.media[0].gif.url,
-		width: result.media[0].mp4.dims[0],
-		height: result.media[0].mp4.dims[1],
-		preview: result.media[0].mp4.preview,
-	};
-}
-
-export function getGifApiKey() {
-	const { enabled, provider, apiKey } = Config.get().gif;
-	if (!enabled) throw new HTTPError(`Gifs are disabled`);
-	if (provider !== "tenor" || !apiKey)
-		throw new HTTPError(`${provider} gif provider not supported`);
-
-	return apiKey;
-}
-
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// TODO: Custom providers
-	// TODO: return gifs as mp4
-	// const { media_format, locale } = req.query;
-	const { locale } = req.query;
-
-	const apiKey = getGifApiKey();
-
-	const agent = new ProxyAgent();
-
-	const [responseSource, trendGifSource] = await Promise.all([
-		fetch(
-			`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`,
-			{
-				agent,
-				method: "get",
-				headers: { "Content-Type": "application/json" },
+router.get(
+	"/",
+	route({
+		query: {
+			locale: {
+				type: "string",
+				description: "Locale",
 			},
-		),
-		fetch(
-			`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`,
-			{
-				agent,
-				method: "get",
-				headers: { "Content-Type": "application/json" },
+		},
+		responses: {
+			200: {
+				body: "TenorTrendingResponse",
 			},
-		),
-	]);
-
-	const { tags } = (await responseSource.json()) as TENOR_CATEGORIES_RESULTS;
-	const { results } = (await trendGifSource.json()) as TENOR_TRENDING_RESULTS;
-
-	res.json({
-		categories: tags.map((x) => ({
-			name: x.searchterm,
-			src: x.image,
-		})),
-		gifs: [parseGifResult(results[0])],
-	}).status(200);
-});
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: Custom providers
+		// TODO: return gifs as mp4
+		// const { media_format, locale } = req.query;
+		const { locale } = req.query;
+
+		const apiKey = getGifApiKey();
+
+		const agent = new ProxyAgent();
+
+		const [responseSource, trendGifSource] = await Promise.all([
+			fetch(
+				`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`,
+				{
+					agent,
+					method: "get",
+					headers: { "Content-Type": "application/json" },
+				},
+			),
+			fetch(
+				`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`,
+				{
+					agent,
+					method: "get",
+					headers: { "Content-Type": "application/json" },
+				},
+			),
+		]);
+
+		const { tags } =
+			(await responseSource.json()) as TenorCategoriesResults;
+		const { results } =
+			(await trendGifSource.json()) as TenorTrendingResults;
+
+		res.json({
+			categories: tags.map((x) => ({
+				name: x.searchterm,
+				src: x.image,
+			})),
+			gifs: [parseGifResult(results[0])],
+		}).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guild-recommendations.ts b/src/api/routes/guild-recommendations.ts
index 67f43c14..876780df 100644
--- a/src/api/routes/guild-recommendations.ts
+++ b/src/api/routes/guild-recommendations.ts
@@ -16,34 +16,44 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Guild, Config } from "@spacebar/util";
+import { Config, Guild } from "@spacebar/util";
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { Like } from "typeorm";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// const { limit, personalization_disabled } = req.query;
-	const { limit } = req.query;
-	const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildRecommendationsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// const { limit, personalization_disabled } = req.query;
+		const { limit } = req.query;
+		const showAllGuilds = Config.get().guild.discovery.showAllGuilds;
 
-	const genLoadId = (size: number) =>
-		[...Array(size)]
-			.map(() => Math.floor(Math.random() * 16).toString(16))
-			.join("");
+		const genLoadId = (size: number) =>
+			[...Array(size)]
+				.map(() => Math.floor(Math.random() * 16).toString(16))
+				.join("");
 
-	const guilds = showAllGuilds
-		? await Guild.find({ take: Math.abs(Number(limit || 24)) })
-		: await Guild.find({
-				where: { features: Like("%DISCOVERABLE%") },
-				take: Math.abs(Number(limit || 24)),
-		  });
-	res.send({
-		recommended_guilds: guilds,
-		load_id: `server_recs/${genLoadId(32)}`,
-	}).status(200);
-});
+		const guilds = showAllGuilds
+			? await Guild.find({ take: Math.abs(Number(limit || 24)) })
+			: await Guild.find({
+					where: { features: Like("%DISCOVERABLE%") },
+					take: Math.abs(Number(limit || 24)),
+			  });
+		res.send({
+			recommended_guilds: guilds,
+			load_id: `server_recs/${genLoadId(32)}`,
+		}).status(200);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts
index 31aed6b9..0776ab62 100644
--- a/src/api/routes/guilds/#guild_id/bans.ts
+++ b/src/api/routes/guilds/#guild_id/bans.ts
@@ -16,20 +16,20 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { getIpAdress, route } from "@spacebar/api";
 import {
+	Ban,
+	BanModeratorSchema,
+	BanRegistrySchema,
 	DiscordApiErrors,
-	emitEvent,
 	GuildBanAddEvent,
 	GuildBanRemoveEvent,
-	Ban,
-	User,
 	Member,
-	BanRegistrySchema,
-	BanModeratorSchema,
+	User,
+	emitEvent,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { getIpAdress, route } from "@spacebar/api";
 
 const router: Router = Router();
 
@@ -37,7 +37,17 @@ const router: Router = Router();
 
 router.get(
 	"/",
-	route({ permission: "BAN_MEMBERS" }),
+	route({
+		permission: "BAN_MEMBERS",
+		responses: {
+			200: {
+				body: "GuildBansResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 
@@ -73,7 +83,20 @@ router.get(
 
 router.get(
 	"/:user",
-	route({ permission: "BAN_MEMBERS" }),
+	route({
+		permission: "BAN_MEMBERS",
+		responses: {
+			200: {
+				body: "BanModeratorSchema",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const user_id = req.params.ban;
@@ -97,7 +120,21 @@ router.get(
 
 router.put(
 	"/:user_id",
-	route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }),
+	route({
+		requestBody: "BanCreateSchema",
+		permission: "BAN_MEMBERS",
+		responses: {
+			200: {
+				body: "Ban",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const banned_user_id = req.params.user_id;
@@ -143,7 +180,20 @@ router.put(
 
 router.put(
 	"/@me",
-	route({ body: "BanCreateSchema" }),
+	route({
+		requestBody: "BanCreateSchema",
+		responses: {
+			200: {
+				body: "Ban",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 
@@ -182,7 +232,18 @@ router.put(
 
 router.delete(
 	"/:user_id",
-	route({ permission: "BAN_MEMBERS" }),
+	route({
+		permission: "BAN_MEMBERS",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, user_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/channels.ts b/src/api/routes/guilds/#guild_id/channels.ts
index d74d9f84..1d5897a5 100644
--- a/src/api/routes/guilds/#guild_id/channels.ts
+++ b/src/api/routes/guilds/#guild_id/channels.ts
@@ -16,28 +16,52 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
-	ChannelUpdateEvent,
-	emitEvent,
 	ChannelModifySchema,
 	ChannelReorderSchema,
+	ChannelUpdateEvent,
+	emitEvent,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	const channels = await Channel.find({ where: { guild_id } });
+router.get(
+	"/",
+	route({
+		responses: {
+			201: {
+				body: "APIChannelArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		const channels = await Channel.find({ where: { guild_id } });
 
-	res.json(channels);
-});
+		res.json(channels);
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }),
+	route({
+		requestBody: "ChannelModifySchema",
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			201: {
+				body: "Channel",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		// creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel
 		const { guild_id } = req.params;
@@ -54,7 +78,19 @@ router.post(
 
 router.patch(
 	"/",
-	route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }),
+	route({
+		requestBody: "ChannelReorderSchema",
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		// changes guild channel position
 		const { guild_id } = req.params;
diff --git a/src/api/routes/guilds/#guild_id/delete.ts b/src/api/routes/guilds/#guild_id/delete.ts
index ec72a4ae..dee52c81 100644
--- a/src/api/routes/guilds/#guild_id/delete.ts
+++ b/src/api/routes/guilds/#guild_id/delete.ts
@@ -16,37 +16,51 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { emitEvent, GuildDeleteEvent, Guild } from "@spacebar/util";
-import { Router, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
 import { route } from "@spacebar/api";
+import { Guild, GuildDeleteEvent, emitEvent } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
 
 const router = Router();
 
 // discord prefixes this route with /delete instead of using the delete method
 // docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild
-router.post("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+			401: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	const guild = await Guild.findOneOrFail({
-		where: { id: guild_id },
-		select: ["owner_id"],
-	});
-	if (guild.owner_id !== req.user_id)
-		throw new HTTPError("You are not the owner of this guild", 401);
+		const guild = await Guild.findOneOrFail({
+			where: { id: guild_id },
+			select: ["owner_id"],
+		});
+		if (guild.owner_id !== req.user_id)
+			throw new HTTPError("You are not the owner of this guild", 401);
 
-	await Promise.all([
-		Guild.delete({ id: guild_id }), // this will also delete all guild related data
-		emitEvent({
-			event: "GUILD_DELETE",
-			data: {
-				id: guild_id,
-			},
-			guild_id: guild_id,
-		} as GuildDeleteEvent),
-	]);
+		await Promise.all([
+			Guild.delete({ id: guild_id }), // this will also delete all guild related data
+			emitEvent({
+				event: "GUILD_DELETE",
+				data: {
+					id: guild_id,
+				},
+				guild_id: guild_id,
+			} as GuildDeleteEvent),
+		]);
 
-	return res.sendStatus(204);
-});
+		return res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/discovery-requirements.ts b/src/api/routes/guilds/#guild_id/discovery-requirements.ts
index 5e15676a..741fa9b3 100644
--- a/src/api/routes/guilds/#guild_id/discovery-requirements.ts
+++ b/src/api/routes/guilds/#guild_id/discovery-requirements.ts
@@ -16,40 +16,50 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	// TODO:
-	// Load from database
-	// Admin control, but for now it allows anyone to be discoverable
-
-	res.send({
-		guild_id: guild_id,
-		safe_environment: true,
-		healthy: true,
-		health_score_pending: false,
-		size: true,
-		nsfw_properties: {},
-		protected: true,
-		sufficient: true,
-		sufficient_without_grace_period: true,
-		valid_rules_channel: true,
-		retention_healthy: true,
-		engagement_healthy: true,
-		age: true,
-		minimum_age: 0,
-		health_score: {
-			avg_nonnew_participators: 0,
-			avg_nonnew_communicators: 0,
-			num_intentful_joiners: 0,
-			perc_ret_w1_intentful: 0,
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildDiscoveryRequirementsResponse",
+			},
 		},
-		minimum_size: 0,
-	});
-});
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		// TODO:
+		// Load from database
+		// Admin control, but for now it allows anyone to be discoverable
+
+		res.send({
+			guild_id: guild_id,
+			safe_environment: true,
+			healthy: true,
+			health_score_pending: false,
+			size: true,
+			nsfw_properties: {},
+			protected: true,
+			sufficient: true,
+			sufficient_without_grace_period: true,
+			valid_rules_channel: true,
+			retention_healthy: true,
+			engagement_healthy: true,
+			age: true,
+			minimum_age: 0,
+			health_score: {
+				avg_nonnew_participators: 0,
+				avg_nonnew_communicators: 0,
+				num_intentful_joiners: 0,
+				perc_ret_w1_intentful: 0,
+			},
+			minimum_size: 0,
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/emojis.ts b/src/api/routes/guilds/#guild_id/emojis.ts
index c661202e..ef28f989 100644
--- a/src/api/routes/guilds/#guild_id/emojis.ts
+++ b/src/api/routes/guilds/#guild_id/emojis.ts
@@ -16,55 +16,95 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
 	Config,
 	DiscordApiErrors,
-	emitEvent,
 	Emoji,
+	EmojiCreateSchema,
+	EmojiModifySchema,
 	GuildEmojisUpdateEvent,
-	handleFile,
 	Member,
 	Snowflake,
 	User,
-	EmojiCreateSchema,
-	EmojiModifySchema,
+	emitEvent,
+	handleFile,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIEmojiArray",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	const emojis = await Emoji.find({
-		where: { guild_id: guild_id },
-		relations: ["user"],
-	});
+		const emojis = await Emoji.find({
+			where: { guild_id: guild_id },
+			relations: ["user"],
+		});
 
-	return res.json(emojis);
-});
+		return res.json(emojis);
+	},
+);
 
-router.get("/:emoji_id", route({}), async (req: Request, res: Response) => {
-	const { guild_id, emoji_id } = req.params;
+router.get(
+	"/:emoji_id",
+	route({
+		responses: {
+			200: {
+				body: "Emoji",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, emoji_id } = req.params;
 
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	const emoji = await Emoji.findOneOrFail({
-		where: { guild_id: guild_id, id: emoji_id },
-		relations: ["user"],
-	});
+		const emoji = await Emoji.findOneOrFail({
+			where: { guild_id: guild_id, id: emoji_id },
+			relations: ["user"],
+		});
 
-	return res.json(emoji);
-});
+		return res.json(emoji);
+	},
+);
 
 router.post(
 	"/",
 	route({
-		body: "EmojiCreateSchema",
+		requestBody: "EmojiCreateSchema",
 		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			201: {
+				body: "Emoji",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
@@ -113,8 +153,16 @@ router.post(
 router.patch(
 	"/:emoji_id",
 	route({
-		body: "EmojiModifySchema",
+		requestBody: "EmojiModifySchema",
 		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			200: {
+				body: "Emoji",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { emoji_id, guild_id } = req.params;
@@ -141,7 +189,15 @@ router.patch(
 
 router.delete(
 	"/:emoji_id",
-	route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }),
+	route({
+		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { emoji_id, guild_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts
index 672bc92e..afe60614 100644
--- a/src/api/routes/guilds/#guild_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/index.ts
@@ -16,46 +16,79 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
 	DiscordApiErrors,
-	emitEvent,
-	getPermission,
-	getRights,
 	Guild,
 	GuildUpdateEvent,
-	handleFile,
-	Member,
 	GuildUpdateSchema,
+	Member,
 	SpacebarApiErrors,
+	emitEvent,
+	getPermission,
+	getRights,
+	handleFile,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-
-	const [guild, member] = await Promise.all([
-		Guild.findOneOrFail({ where: { id: guild_id } }),
-		Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }),
-	]);
-	if (!member)
-		throw new HTTPError(
-			"You are not a member of the guild you are trying to access",
-			401,
-		);
-
-	return res.send({
-		...guild,
-		joined_at: member?.joined_at,
-	});
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			"200": {
+				body: "APIGuildWithJoinedAt",
+			},
+			401: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+
+		const [guild, member] = await Promise.all([
+			Guild.findOneOrFail({ where: { id: guild_id } }),
+			Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }),
+		]);
+		if (!member)
+			throw new HTTPError(
+				"You are not a member of the guild you are trying to access",
+				401,
+			);
+
+		return res.send({
+			...guild,
+			joined_at: member?.joined_at,
+		});
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "GuildUpdateSchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "GuildUpdateSchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			"200": {
+				body: "GuildUpdateSchema",
+			},
+			401: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as GuildUpdateSchema;
 		const { guild_id } = req.params;
diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts
index 9c446928..a0ffa3f4 100644
--- a/src/api/routes/guilds/#guild_id/invites.ts
+++ b/src/api/routes/guilds/#guild_id/invites.ts
@@ -16,15 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Invite, PublicInviteRelation } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { Invite, PublicInviteRelation } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.get(
 	"/",
-	route({ permission: "MANAGE_GUILD" }),
+	route({
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "APIInviteArray",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/member-verification.ts b/src/api/routes/guilds/#guild_id/member-verification.ts
index 242f3684..2c39093e 100644
--- a/src/api/routes/guilds/#guild_id/member-verification.ts
+++ b/src/api/routes/guilds/#guild_id/member-verification.ts
@@ -16,17 +16,27 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// TODO: member verification
+router.get(
+	"/",
+	route({
+		responses: {
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: member verification
 
-	res.status(404).json({
-		message: "Unknown Guild Member Verification Form",
-		code: 10068,
-	});
-});
+		res.status(404).json({
+			message: "Unknown Guild Member Verification Form",
+			code: 10068,
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
index a14691f2..cafb922e 100644
--- a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -16,38 +16,91 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
-	Member,
-	getPermission,
-	getRights,
-	Role,
-	GuildMemberUpdateEvent,
 	emitEvent,
-	Sticker,
 	Emoji,
+	getPermission,
+	getRights,
 	Guild,
+	GuildMemberUpdateEvent,
 	handleFile,
+	Member,
 	MemberChangeSchema,
+	PublicMemberProjection,
+	PublicUserProjection,
+	Role,
+	Sticker,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id, member_id } = req.params;
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIPublicMember",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, member_id } = req.params;
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	const member = await Member.findOneOrFail({
-		where: { id: member_id, guild_id },
-	});
+		const member = await Member.findOneOrFail({
+			where: { id: member_id, guild_id },
+			relations: ["roles", "user"],
+			select: {
+				index: true,
+				// only grab public member props
+				...Object.fromEntries(
+					PublicMemberProjection.map((x) => [x, true]),
+				),
+				// and public user props
+				user: Object.fromEntries(
+					PublicUserProjection.map((x) => [x, true]),
+				),
+				roles: {
+					id: true,
+				},
+			},
+		});
 
-	return res.json(member);
-});
+		return res.json({
+			...member.toPublicMember(),
+			user: member.user.toPublicUser(),
+			roles: member.roles.map((x) => x.id),
+		});
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "MemberChangeSchema" }),
+	route({
+		requestBody: "MemberChangeSchema",
+		responses: {
+			200: {
+				body: "Member",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const member_id =
@@ -119,54 +172,81 @@ router.patch(
 	},
 );
 
-router.put("/", route({}), async (req: Request, res: Response) => {
-	// TODO: Lurker mode
-
-	const rights = await getRights(req.user_id);
-
-	const { guild_id } = req.params;
-	let { member_id } = req.params;
-	if (member_id === "@me") {
-		member_id = req.user_id;
-		rights.hasThrow("JOIN_GUILDS");
-	} else {
-		// TODO: join others by controller
-	}
-
-	const guild = await Guild.findOneOrFail({
-		where: { id: guild_id },
-	});
-
-	const emoji = await Emoji.find({
-		where: { guild_id: guild_id },
-	});
-
-	const roles = await Role.find({
-		where: { guild_id: guild_id },
-	});
-
-	const stickers = await Sticker.find({
-		where: { guild_id: guild_id },
-	});
-
-	await Member.addToGuild(member_id, guild_id);
-	res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers });
-});
-
-router.delete("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id, member_id } = req.params;
-	const permission = await getPermission(req.user_id, guild_id);
-	const rights = await getRights(req.user_id);
-	if (member_id === "@me" || member_id === req.user_id) {
-		// TODO: unless force-joined
-		rights.hasThrow("SELF_LEAVE_GROUPS");
-	} else {
-		rights.hasThrow("KICK_BAN_MEMBERS");
-		permission.hasThrow("KICK_MEMBERS");
-	}
-
-	await Member.removeFromGuild(member_id, guild_id);
-	res.sendStatus(204);
-});
+router.put(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "MemberJoinGuildResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		// TODO: Lurker mode
+
+		const rights = await getRights(req.user_id);
+
+		const { guild_id } = req.params;
+		let { member_id } = req.params;
+		if (member_id === "@me") {
+			member_id = req.user_id;
+			rights.hasThrow("JOIN_GUILDS");
+		} else {
+			// TODO: join others by controller
+		}
+
+		const guild = await Guild.findOneOrFail({
+			where: { id: guild_id },
+		});
+
+		const emoji = await Emoji.find({
+			where: { guild_id: guild_id },
+		});
+
+		const roles = await Role.find({
+			where: { guild_id: guild_id },
+		});
+
+		const stickers = await Sticker.find({
+			where: { guild_id: guild_id },
+		});
+
+		await Member.addToGuild(member_id, guild_id);
+		res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers });
+	},
+);
+
+router.delete(
+	"/",
+	route({
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, member_id } = req.params;
+		const permission = await getPermission(req.user_id, guild_id);
+		const rights = await getRights(req.user_id);
+		if (member_id === "@me" || member_id === req.user_id) {
+			// TODO: unless force-joined
+			rights.hasThrow("SELF_LEAVE_GROUPS");
+		} else {
+			rights.hasThrow("KICK_BAN_MEMBERS");
+			permission.hasThrow("KICK_MEMBERS");
+		}
+
+		await Member.removeFromGuild(member_id, guild_id);
+		res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
index 14e7467f..7b8e44d3 100644
--- a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
@@ -16,15 +16,26 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { getPermission, Member, PermissionResolvable } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { getPermission, Member, PermissionResolvable } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.patch(
 	"/",
-	route({ body: "MemberNickChangeSchema" }),
+	route({
+		requestBody: "MemberNickChangeSchema",
+		responses: {
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		let permissionString: PermissionResolvable = "MANAGE_NICKNAMES";
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
index 698df88f..46dd70bb 100644
--- a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
@@ -16,15 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Member } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { Member } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_ROLES" }),
+	route({
+		permission: "MANAGE_ROLES",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, role_id, member_id } = req.params;
 
@@ -35,7 +43,13 @@ router.delete(
 
 router.put(
 	"/",
-	route({ permission: "MANAGE_ROLES" }),
+	route({
+		permission: "MANAGE_ROLES",
+		responses: {
+			204: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, role_id, member_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/members/index.ts b/src/api/routes/guilds/#guild_id/members/index.ts
index f7a55cf1..9260308d 100644
--- a/src/api/routes/guilds/#guild_id/members/index.ts
+++ b/src/api/routes/guilds/#guild_id/members/index.ts
@@ -16,35 +16,58 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
-import { Member, PublicMemberProjection } from "@spacebar/util";
 import { route } from "@spacebar/api";
-import { MoreThan } from "typeorm";
+import { Member, PublicMemberProjection } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import { MoreThan } from "typeorm";
 
 const router = Router();
 
 // TODO: send over websocket
 // TODO: check for GUILD_MEMBERS intent
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	const limit = Number(req.query.limit) || 1;
-	if (limit > 1000 || limit < 1)
-		throw new HTTPError("Limit must be between 1 and 1000");
-	const after = `${req.query.after}`;
-	const query = after ? { id: MoreThan(after) } : {};
-
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
-
-	const members = await Member.find({
-		where: { guild_id, ...query },
-		select: PublicMemberProjection,
-		take: limit,
-		order: { id: "ASC" },
-	});
-
-	return res.json(members);
-});
+router.get(
+	"/",
+	route({
+		query: {
+			limit: {
+				type: "number",
+				description:
+					"max number of members to return (1-1000). default 1",
+			},
+			after: {
+				type: "string",
+			},
+		},
+		responses: {
+			200: {
+				body: "APIMemberArray",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		const limit = Number(req.query.limit) || 1;
+		if (limit > 1000 || limit < 1)
+			throw new HTTPError("Limit must be between 1 and 1000");
+		const after = `${req.query.after}`;
+		const query = after ? { id: MoreThan(after) } : {};
+
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+		const members = await Member.find({
+			where: { guild_id, ...query },
+			select: PublicMemberProjection,
+			take: limit,
+			order: { id: "ASC" },
+		});
+
+		return res.json(members);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/messages/search.ts b/src/api/routes/guilds/#guild_id/messages/search.ts
index bc5f1b6e..637d1e43 100644
--- a/src/api/routes/guilds/#guild_id/messages/search.ts
+++ b/src/api/routes/guilds/#guild_id/messages/search.ts
@@ -18,140 +18,159 @@
 
 /* eslint-disable @typescript-eslint/ban-ts-comment */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
-import { getPermission, FieldErrors, Message, Channel } from "@spacebar/util";
+import { Channel, FieldErrors, Message, getPermission } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { FindManyOptions, In, Like } from "typeorm";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const {
-		channel_id,
-		content,
-		// include_nsfw, // TODO
-		offset,
-		sort_order,
-		// sort_by, // TODO: Handle 'relevance'
-		limit,
-		author_id,
-	} = req.query;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildMessagesSearchResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			422: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const {
+			channel_id,
+			content,
+			// include_nsfw, // TODO
+			offset,
+			sort_order,
+			// sort_by, // TODO: Handle 'relevance'
+			limit,
+			author_id,
+		} = req.query;
 
-	const parsedLimit = Number(limit) || 50;
-	if (parsedLimit < 1 || parsedLimit > 100)
-		throw new HTTPError("limit must be between 1 and 100", 422);
+		const parsedLimit = Number(limit) || 50;
+		if (parsedLimit < 1 || parsedLimit > 100)
+			throw new HTTPError("limit must be between 1 and 100", 422);
 
-	if (sort_order) {
-		if (
-			typeof sort_order != "string" ||
-			["desc", "asc"].indexOf(sort_order) == -1
-		)
-			throw FieldErrors({
-				sort_order: {
-					message: "Value must be one of ('desc', 'asc').",
-					code: "BASE_TYPE_CHOICES",
-				},
-			}); // todo this is wrong
-	}
+		if (sort_order) {
+			if (
+				typeof sort_order != "string" ||
+				["desc", "asc"].indexOf(sort_order) == -1
+			)
+				throw FieldErrors({
+					sort_order: {
+						message: "Value must be one of ('desc', 'asc').",
+						code: "BASE_TYPE_CHOICES",
+					},
+				}); // todo this is wrong
+		}
 
-	const permissions = await getPermission(
-		req.user_id,
-		req.params.guild_id,
-		channel_id as string | undefined,
-	);
-	permissions.hasThrow("VIEW_CHANNEL");
-	if (!permissions.has("READ_MESSAGE_HISTORY"))
-		return res.json({ messages: [], total_results: 0 });
+		const permissions = await getPermission(
+			req.user_id,
+			req.params.guild_id,
+			channel_id as string | undefined,
+		);
+		permissions.hasThrow("VIEW_CHANNEL");
+		if (!permissions.has("READ_MESSAGE_HISTORY"))
+			return res.json({ messages: [], total_results: 0 });
 
-	const query: FindManyOptions<Message> = {
-		order: {
-			timestamp: sort_order
-				? (sort_order.toUpperCase() as "ASC" | "DESC")
-				: "DESC",
-		},
-		take: parsedLimit || 0,
-		where: {
-			guild: {
-				id: req.params.guild_id,
+		const query: FindManyOptions<Message> = {
+			order: {
+				timestamp: sort_order
+					? (sort_order.toUpperCase() as "ASC" | "DESC")
+					: "DESC",
 			},
-		},
-		relations: [
-			"author",
-			"webhook",
-			"application",
-			"mentions",
-			"mention_roles",
-			"mention_channels",
-			"sticker_items",
-			"attachments",
-		],
-		skip: offset ? Number(offset) : 0,
-	};
-	//@ts-ignore
-	if (channel_id) query.where.channel = { id: channel_id };
-	else {
-		// get all channel IDs that this user can access
-		const channels = await Channel.find({
-			where: { guild_id: req.params.guild_id },
-			select: ["id"],
-		});
-		const ids = [];
+			take: parsedLimit || 0,
+			where: {
+				guild: {
+					id: req.params.guild_id,
+				},
+			},
+			relations: [
+				"author",
+				"webhook",
+				"application",
+				"mentions",
+				"mention_roles",
+				"mention_channels",
+				"sticker_items",
+				"attachments",
+			],
+			skip: offset ? Number(offset) : 0,
+		};
+		//@ts-ignore
+		if (channel_id) query.where.channel = { id: channel_id };
+		else {
+			// get all channel IDs that this user can access
+			const channels = await Channel.find({
+				where: { guild_id: req.params.guild_id },
+				select: ["id"],
+			});
+			const ids = [];
 
-		for (const channel of channels) {
-			const perm = await getPermission(
-				req.user_id,
-				req.params.guild_id,
-				channel.id,
-			);
-			if (!perm.has("VIEW_CHANNEL") || !perm.has("READ_MESSAGE_HISTORY"))
-				continue;
-			ids.push(channel.id);
-		}
+			for (const channel of channels) {
+				const perm = await getPermission(
+					req.user_id,
+					req.params.guild_id,
+					channel.id,
+				);
+				if (
+					!perm.has("VIEW_CHANNEL") ||
+					!perm.has("READ_MESSAGE_HISTORY")
+				)
+					continue;
+				ids.push(channel.id);
+			}
 
+			//@ts-ignore
+			query.where.channel = { id: In(ids) };
+		}
+		//@ts-ignore
+		if (author_id) query.where.author = { id: author_id };
 		//@ts-ignore
-		query.where.channel = { id: In(ids) };
-	}
-	//@ts-ignore
-	if (author_id) query.where.author = { id: author_id };
-	//@ts-ignore
-	if (content) query.where.content = Like(`%${content}%`);
+		if (content) query.where.content = Like(`%${content}%`);
 
-	const messages: Message[] = await Message.find(query);
+		const messages: Message[] = await Message.find(query);
 
-	const messagesDto = messages.map((x) => [
-		{
-			id: x.id,
-			type: x.type,
-			content: x.content,
-			channel_id: x.channel_id,
-			author: {
-				id: x.author?.id,
-				username: x.author?.username,
-				avatar: x.author?.avatar,
-				avatar_decoration: null,
-				discriminator: x.author?.discriminator,
-				public_flags: x.author?.public_flags,
+		const messagesDto = messages.map((x) => [
+			{
+				id: x.id,
+				type: x.type,
+				content: x.content,
+				channel_id: x.channel_id,
+				author: {
+					id: x.author?.id,
+					username: x.author?.username,
+					avatar: x.author?.avatar,
+					avatar_decoration: null,
+					discriminator: x.author?.discriminator,
+					public_flags: x.author?.public_flags,
+				},
+				attachments: x.attachments,
+				embeds: x.embeds,
+				mentions: x.mentions,
+				mention_roles: x.mention_roles,
+				pinned: x.pinned,
+				mention_everyone: x.mention_everyone,
+				tts: x.tts,
+				timestamp: x.timestamp,
+				edited_timestamp: x.edited_timestamp,
+				flags: x.flags,
+				components: x.components,
+				hit: true,
 			},
-			attachments: x.attachments,
-			embeds: x.embeds,
-			mentions: x.mentions,
-			mention_roles: x.mention_roles,
-			pinned: x.pinned,
-			mention_everyone: x.mention_everyone,
-			tts: x.tts,
-			timestamp: x.timestamp,
-			edited_timestamp: x.edited_timestamp,
-			flags: x.flags,
-			components: x.components,
-			hit: true,
-		},
-	]);
+		]);
 
-	return res.json({
-		messages: messagesDto,
-		total_results: messages.length,
-	});
-});
+		return res.json({
+			messages: messagesDto,
+			total_results: messages.length,
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/profile/index.ts b/src/api/routes/guilds/#guild_id/profile/index.ts
index 8ec22ea4..60526259 100644
--- a/src/api/routes/guilds/#guild_id/profile/index.ts
+++ b/src/api/routes/guilds/#guild_id/profile/index.ts
@@ -31,7 +31,20 @@ const router = Router();
 
 router.patch(
 	"/:member_id",
-	route({ body: "MemberChangeProfileSchema" }),
+	route({
+		requestBody: "MemberChangeProfileSchema",
+		responses: {
+			200: {
+				body: "Member",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		// const member_id =
diff --git a/src/api/routes/guilds/#guild_id/prune.ts b/src/api/routes/guilds/#guild_id/prune.ts
index dbed546b..2c77340d 100644
--- a/src/api/routes/guilds/#guild_id/prune.ts
+++ b/src/api/routes/guilds/#guild_id/prune.ts
@@ -16,14 +16,14 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { Guild, Member, Snowflake } from "@spacebar/util";
-import { LessThan, IsNull } from "typeorm";
 import { route } from "@spacebar/api";
+import { Guild, Member, Snowflake } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { IsNull, LessThan } from "typeorm";
 const router = Router();
 
 //Returns all inactive members, respecting role hierarchy
-export const inactiveMembers = async (
+const inactiveMembers = async (
 	guild_id: string,
 	user_id: string,
 	days: number,
@@ -80,25 +80,46 @@ export const inactiveMembers = async (
 	return members;
 };
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const days = parseInt(req.query.days as string);
+router.get(
+	"/",
+	route({
+		responses: {
+			"200": {
+				body: "GuildPruneResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const days = parseInt(req.query.days as string);
 
-	let roles = req.query.include_roles;
-	if (typeof roles === "string") roles = [roles]; //express will return array otherwise
+		let roles = req.query.include_roles;
+		if (typeof roles === "string") roles = [roles]; //express will return array otherwise
 
-	const members = await inactiveMembers(
-		req.params.guild_id,
-		req.user_id,
-		days,
-		roles as string[],
-	);
+		const members = await inactiveMembers(
+			req.params.guild_id,
+			req.user_id,
+			days,
+			roles as string[],
+		);
 
-	res.send({ pruned: members.length });
-});
+		res.send({ pruned: members.length });
+	},
+);
 
 router.post(
 	"/",
-	route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }),
+	route({
+		permission: "KICK_MEMBERS",
+		right: "KICK_BAN_MEMBERS",
+		responses: {
+			200: {
+				body: "GuildPurgeResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const days = parseInt(req.body.days);
 
diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts
index de1e8769..b0ae0602 100644
--- a/src/api/routes/guilds/#guild_id/regions.ts
+++ b/src/api/routes/guilds/#guild_id/regions.ts
@@ -16,22 +16,35 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
 import { Guild } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { getVoiceRegions, route, getIpAdress } from "@spacebar/api";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
-	//TODO we should use an enum for guild's features and not hardcoded strings
-	return res.json(
-		await getVoiceRegions(
-			getIpAdress(req),
-			guild.features.includes("VIP_REGIONS"),
-		),
-	);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIGuildVoiceRegion",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		//TODO we should use an enum for guild's features and not hardcoded strings
+		return res.json(
+			await getVoiceRegions(
+				getIpAdress(req),
+				guild.features.includes("VIP_REGIONS"),
+			),
+		);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
index de3fc35b..ea1a782a 100644
--- a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
@@ -16,31 +16,63 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
-	Role,
-	Member,
-	GuildRoleUpdateEvent,
-	GuildRoleDeleteEvent,
 	emitEvent,
+	GuildRoleDeleteEvent,
+	GuildRoleUpdateEvent,
 	handleFile,
+	Member,
+	Role,
 	RoleModifySchema,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id, role_id } = req.params;
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
-	const role = await Role.findOneOrFail({ where: { guild_id, id: role_id } });
-	return res.json(role);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "Role",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, role_id } = req.params;
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
+		const role = await Role.findOneOrFail({
+			where: { guild_id, id: role_id },
+		});
+		return res.json(role);
+	},
+);
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_ROLES" }),
+	route({
+		permission: "MANAGE_ROLES",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, role_id } = req.params;
 		if (role_id === guild_id)
@@ -69,7 +101,24 @@ router.delete(
 
 router.patch(
 	"/",
-	route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }),
+	route({
+		requestBody: "RoleModifySchema",
+		permission: "MANAGE_ROLES",
+		responses: {
+			200: {
+				body: "Role",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { role_id, guild_id } = req.params;
 		const body = req.body as RoleModifySchema;
diff --git a/src/api/routes/guilds/#guild_id/roles/index.ts b/src/api/routes/guilds/#guild_id/roles/index.ts
index f93e9385..e2c34e7f 100644
--- a/src/api/routes/guilds/#guild_id/roles/index.ts
+++ b/src/api/routes/guilds/#guild_id/roles/index.ts
@@ -16,21 +16,20 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
-	Role,
-	getPermission,
-	Member,
-	GuildRoleCreateEvent,
-	GuildRoleUpdateEvent,
-	emitEvent,
 	Config,
 	DiscordApiErrors,
+	emitEvent,
+	GuildRoleCreateEvent,
+	GuildRoleUpdateEvent,
+	Member,
+	Role,
 	RoleModifySchema,
 	RolePositionUpdateSchema,
 	Snowflake,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { Not } from "typeorm";
 
 const router: Router = Router();
@@ -47,7 +46,21 @@ router.get("/", route({}), async (req: Request, res: Response) => {
 
 router.post(
 	"/",
-	route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }),
+	route({
+		requestBody: "RoleModifySchema",
+		permission: "MANAGE_ROLES",
+		responses: {
+			200: {
+				body: "Role",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const guild_id = req.params.guild_id;
 		const body = req.body as RoleModifySchema;
@@ -104,14 +117,25 @@ router.post(
 
 router.patch(
 	"/",
-	route({ body: "RolePositionUpdateSchema" }),
+	route({
+		requestBody: "RolePositionUpdateSchema",
+		permission: "MANAGE_ROLES",
+		responses: {
+			200: {
+				body: "APIRoleArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const body = req.body as RolePositionUpdateSchema;
 
-		const perms = await getPermission(req.user_id, guild_id);
-		perms.hasThrow("MANAGE_ROLES");
-
 		await Promise.all(
 			body.map(async (x) =>
 				Role.update({ guild_id, id: x.id }, { position: x.position }),
diff --git a/src/api/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts
index 84a23670..88f9a40e 100644
--- a/src/api/routes/guilds/#guild_id/stickers.ts
+++ b/src/api/routes/guilds/#guild_id/stickers.ts
@@ -16,29 +16,42 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
-	emitEvent,
 	GuildStickersUpdateEvent,
 	Member,
+	ModifyGuildStickerSchema,
 	Snowflake,
 	Sticker,
 	StickerFormatType,
 	StickerType,
+	emitEvent,
 	uploadFile,
-	ModifyGuildStickerSchema,
 } from "@spacebar/util";
-import { Router, Request, Response } from "express";
-import { route } from "@spacebar/api";
-import multer from "multer";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import multer from "multer";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIStickerArray",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	res.json(await Sticker.find({ where: { guild_id } }));
-});
+		res.json(await Sticker.find({ where: { guild_id } }));
+	},
+);
 
 const bodyParser = multer({
 	limits: {
@@ -54,7 +67,18 @@ router.post(
 	bodyParser,
 	route({
 		permission: "MANAGE_EMOJIS_AND_STICKERS",
-		body: "ModifyGuildStickerSchema",
+		requestBody: "ModifyGuildStickerSchema",
+		responses: {
+			200: {
+				body: "Sticker",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		if (!req.file) throw new HTTPError("missing file");
@@ -81,7 +105,7 @@ router.post(
 	},
 );
 
-export function getStickerFormat(mime_type: string) {
+function getStickerFormat(mime_type: string) {
 	switch (mime_type) {
 		case "image/apng":
 			return StickerFormatType.APNG;
@@ -98,20 +122,46 @@ export function getStickerFormat(mime_type: string) {
 	}
 }
 
-router.get("/:sticker_id", route({}), async (req: Request, res: Response) => {
-	const { guild_id, sticker_id } = req.params;
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+router.get(
+	"/:sticker_id",
+	route({
+		responses: {
+			200: {
+				body: "Sticker",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id, sticker_id } = req.params;
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	res.json(
-		await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } }),
-	);
-});
+		res.json(
+			await Sticker.findOneOrFail({
+				where: { guild_id, id: sticker_id },
+			}),
+		);
+	},
+);
 
 router.patch(
 	"/:sticker_id",
 	route({
-		body: "ModifyGuildStickerSchema",
+		requestBody: "ModifyGuildStickerSchema",
 		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			200: {
+				body: "Sticker",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, sticker_id } = req.params;
@@ -141,7 +191,15 @@ async function sendStickerUpdateEvent(guild_id: string) {
 
 router.delete(
 	"/:sticker_id",
-	route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }),
+	route({
+		permission: "MANAGE_EMOJIS_AND_STICKERS",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id, sticker_id } = req.params;
 
diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts
index 3bd28e05..85ae0ac9 100644
--- a/src/api/routes/guilds/#guild_id/templates.ts
+++ b/src/api/routes/guilds/#guild_id/templates.ts
@@ -16,11 +16,10 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { generateCode, route } from "@spacebar/api";
 import { Guild, Template } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
-import { generateCode } from "@spacebar/api";
 
 const router: Router = Router();
 
@@ -41,19 +40,46 @@ const TemplateGuildProjection: (keyof Guild)[] = [
 	"icon",
 ];
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APITemplateArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	const templates = await Template.find({
-		where: { source_guild_id: guild_id },
-	});
+		const templates = await Template.find({
+			where: { source_guild_id: guild_id },
+		});
 
-	return res.json(templates);
-});
+		return res.json(templates);
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "TemplateCreateSchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "Template",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const guild = await Guild.findOneOrFail({
@@ -81,7 +107,13 @@ router.post(
 
 router.delete(
 	"/:code",
-	route({ permission: "MANAGE_GUILD" }),
+	route({
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: { body: "Template" },
+			403: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { code, guild_id } = req.params;
 
@@ -96,7 +128,13 @@ router.delete(
 
 router.put(
 	"/:code",
-	route({ permission: "MANAGE_GUILD" }),
+	route({
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: { body: "Template" },
+			403: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { code, guild_id } = req.params;
 		const guild = await Guild.findOneOrFail({
@@ -115,7 +153,14 @@ router.put(
 
 router.patch(
 	"/:code",
-	route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "TemplateModifySchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: { body: "Template" },
+			403: { body: "APIErrorResponse" },
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { code, guild_id } = req.params;
 		const { name, description } = req.body;
diff --git a/src/api/routes/guilds/#guild_id/vanity-url.ts b/src/api/routes/guilds/#guild_id/vanity-url.ts
index c85c943f..a64ae2c9 100644
--- a/src/api/routes/guilds/#guild_id/vanity-url.ts
+++ b/src/api/routes/guilds/#guild_id/vanity-url.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelType,
@@ -23,8 +24,7 @@ import {
 	Invite,
 	VanityUrlSchema,
 } from "@spacebar/util";
-import { Router, Request, Response } from "express";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 const router = Router();
@@ -33,7 +33,20 @@ const InviteRegex = /\W/g;
 
 router.get(
 	"/",
-	route({ permission: "MANAGE_GUILD" }),
+	route({
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "GuildVanityUrlResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
@@ -60,7 +73,21 @@ router.get(
 
 router.patch(
 	"/",
-	route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "VanityUrlSchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "GuildVanityUrlCreateResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { guild_id } = req.params;
 		const body = req.body as VanityUrlSchema;
@@ -80,6 +107,17 @@ router.patch(
 			where: { guild_id, type: ChannelType.GUILD_TEXT },
 		});
 
+		if (!guild.features.includes("ALIASABLE_NAMES")) {
+			await Invite.update(
+				{ guild_id },
+				{
+					code: code,
+				},
+			);
+
+			return res.json({ code });
+		}
+
 		await Invite.create({
 			vanity_url: true,
 			code: code,
diff --git a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
index 791ac102..60c69075 100644
--- a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelType,
@@ -26,7 +27,6 @@ import {
 	VoiceStateUpdateEvent,
 	VoiceStateUpdateSchema,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
 import { Request, Response, Router } from "express";
 
 const router = Router();
@@ -34,7 +34,21 @@ const router = Router();
 
 router.patch(
 	"/",
-	route({ body: "VoiceStateUpdateSchema" }),
+	route({
+		requestBody: "VoiceStateUpdateSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as VoiceStateUpdateSchema;
 		const { guild_id } = req.params;
diff --git a/src/api/routes/guilds/#guild_id/welcome-screen.ts b/src/api/routes/guilds/#guild_id/welcome-screen.ts
index 696e20db..2a739683 100644
--- a/src/api/routes/guilds/#guild_id/welcome-screen.ts
+++ b/src/api/routes/guilds/#guild_id/welcome-screen.ts
@@ -16,27 +16,49 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
+import { Guild, GuildUpdateWelcomeScreenSchema, Member } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { Guild, Member, GuildUpdateWelcomeScreenSchema } from "@spacebar/util";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const guild_id = req.params.guild_id;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildWelcomeScreen",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const guild_id = req.params.guild_id;
 
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
-	await Member.IsInGuildOrFail(req.user_id, guild_id);
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	res.json(guild.welcome_screen);
-});
+		res.json(guild.welcome_screen);
+	},
+);
 
 router.patch(
 	"/",
 	route({
-		body: "GuildUpdateWelcomeScreenSchema",
+		requestBody: "GuildUpdateWelcomeScreenSchema",
 		permission: "MANAGE_GUILD",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const guild_id = req.params.guild_id;
diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts
index 1799f0be..69b5d48c 100644
--- a/src/api/routes/guilds/#guild_id/widget.json.ts
+++ b/src/api/routes/guilds/#guild_id/widget.json.ts
@@ -16,10 +16,10 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { random, route } from "@spacebar/api";
+import { Channel, Guild, Invite, Member, Permissions } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { Permissions, Guild, Invite, Channel, Member } from "@spacebar/util";
 import { HTTPError } from "lambert-server";
-import { random, route } from "@spacebar/api";
 
 const router: Router = Router();
 
@@ -32,77 +32,90 @@ const router: Router = Router();
 
 // https://discord.com/developers/docs/resources/guild#get-guild-widget
 // TODO: Cache the response for a guild for 5 minutes regardless of response
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildWidgetJsonResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
-	if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
 
-	// Fetch existing widget invite for widget channel
-	let invite = await Invite.findOne({
-		where: { channel_id: guild.widget_channel_id },
-	});
+		// Fetch existing widget invite for widget channel
+		let invite = await Invite.findOne({
+			where: { channel_id: guild.widget_channel_id },
+		});
 
-	if (guild.widget_channel_id && !invite) {
-		// Create invite for channel if none exists
-		// TODO: Refactor invite create code to a shared function
-		const max_age = 86400; // 24 hours
-		const expires_at = new Date(max_age * 1000 + Date.now());
+		if (guild.widget_channel_id && !invite) {
+			// Create invite for channel if none exists
+			// TODO: Refactor invite create code to a shared function
+			const max_age = 86400; // 24 hours
+			const expires_at = new Date(max_age * 1000 + Date.now());
 
-		invite = await Invite.create({
-			code: random(),
-			temporary: false,
-			uses: 0,
-			max_uses: 0,
-			max_age: max_age,
-			expires_at,
-			created_at: new Date(),
-			guild_id,
-			channel_id: guild.widget_channel_id,
-		}).save();
-	}
+			invite = await Invite.create({
+				code: random(),
+				temporary: false,
+				uses: 0,
+				max_uses: 0,
+				max_age: max_age,
+				expires_at,
+				created_at: new Date(),
+				guild_id,
+				channel_id: guild.widget_channel_id,
+			}).save();
+		}
 
-	// Fetch voice channels, and the @everyone permissions object
-	const channels: { id: string; name: string; position: number }[] = [];
+		// Fetch voice channels, and the @everyone permissions object
+		const channels: { id: string; name: string; position: number }[] = [];
 
-	(
-		await Channel.find({
-			where: { guild_id: guild_id, type: 2 },
-			order: { position: "ASC" },
-		})
-	).filter((doc) => {
-		// Only return channels where @everyone has the CONNECT permission
-		if (
-			doc.permission_overwrites === undefined ||
-			Permissions.channelPermission(
-				doc.permission_overwrites,
-				Permissions.FLAGS.CONNECT,
-			) === Permissions.FLAGS.CONNECT
-		) {
-			channels.push({
-				id: doc.id,
-				name: doc.name ?? "Unknown channel",
-				position: doc.position ?? 0,
-			});
-		}
-	});
+		(
+			await Channel.find({
+				where: { guild_id: guild_id, type: 2 },
+				order: { position: "ASC" },
+			})
+		).filter((doc) => {
+			// Only return channels where @everyone has the CONNECT permission
+			if (
+				doc.permission_overwrites === undefined ||
+				Permissions.channelPermission(
+					doc.permission_overwrites,
+					Permissions.FLAGS.CONNECT,
+				) === Permissions.FLAGS.CONNECT
+			) {
+				channels.push({
+					id: doc.id,
+					name: doc.name ?? "Unknown channel",
+					position: doc.position ?? 0,
+				});
+			}
+		});
 
-	// Fetch members
-	// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
-	const members = await Member.find({ where: { guild_id: guild_id } });
+		// Fetch members
+		// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
+		const members = await Member.find({ where: { guild_id: guild_id } });
 
-	// Construct object to respond with
-	const data = {
-		id: guild_id,
-		name: guild.name,
-		instant_invite: invite?.code,
-		channels: channels,
-		members: members,
-		presence_count: guild.presence_count,
-	};
+		// Construct object to respond with
+		const data = {
+			id: guild_id,
+			name: guild.name,
+			instant_invite: invite?.code,
+			channels: channels,
+			members: members,
+			presence_count: guild.presence_count,
+		};
 
-	res.set("Cache-Control", "public, max-age=300");
-	return res.json(data);
-});
+		res.set("Cache-Control", "public, max-age=300");
+		return res.json(data);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/guilds/#guild_id/widget.png.ts b/src/api/routes/guilds/#guild_id/widget.png.ts
index 4e975603..c9ba8afc 100644
--- a/src/api/routes/guilds/#guild_id/widget.png.ts
+++ b/src/api/routes/guilds/#guild_id/widget.png.ts
@@ -18,11 +18,11 @@
 
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
-import { Request, Response, Router } from "express";
-import { Guild } from "@spacebar/util";
-import { HTTPError } from "lambert-server";
 import { route } from "@spacebar/api";
+import { Guild } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import fs from "fs";
+import { HTTPError } from "lambert-server";
 import path from "path";
 
 const router: Router = Router();
@@ -31,130 +31,178 @@ const router: Router = Router();
 
 // https://discord.com/developers/docs/resources/guild#get-guild-widget-image
 // TODO: Cache the response
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
-
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
-	if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
-
-	// Fetch guild information
-	const icon = guild.icon;
-	const name = guild.name;
-	const presence = guild.presence_count + " ONLINE";
-
-	// Fetch parameter
-	const style = req.query.style?.toString() || "shield";
-	if (
-		!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)
-	) {
-		throw new HTTPError(
-			"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
-			400,
-		);
-	}
-
-	// Setup canvas
-	const { createCanvas } = require("canvas");
-	const { loadImage } = require("canvas");
-	const sizeOf = require("image-size");
-
-	// TODO: Widget style templates need Spacebar branding
-	const source = path.join(
-		__dirname,
-		"..",
-		"..",
-		"..",
-		"..",
-		"..",
-		"assets",
-		"widget",
-		`${style}.png`,
-	);
-	if (!fs.existsSync(source)) {
-		throw new HTTPError("Widget template does not exist.", 400);
-	}
-
-	// Create base template image for parameter
-	const { width, height } = await sizeOf(source);
-	const canvas = createCanvas(width, height);
-	const ctx = canvas.getContext("2d");
-	const template = await loadImage(source);
-	ctx.drawImage(template, 0, 0);
-
-	// Add the guild specific information to the template asset image
-	switch (style) {
-		case "shield":
-			ctx.textAlign = "center";
-			await drawText(
-				ctx,
-				73,
-				13,
-				"#FFFFFF",
-				"thin 10px Verdana",
-				presence,
-			);
-			break;
-		case "banner1":
-			if (icon) await drawIcon(ctx, 20, 27, 50, icon);
-			await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22);
-			await drawText(
-				ctx,
-				83,
-				66,
-				"#C9D2F0FF",
-				"thin 11px Verdana",
-				presence,
-			);
-			break;
-		case "banner2":
-			if (icon) await drawIcon(ctx, 13, 19, 36, icon);
-			await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15);
-			await drawText(
-				ctx,
-				62,
-				49,
-				"#C9D2F0FF",
-				"thin 11px Verdana",
-				presence,
-			);
-			break;
-		case "banner3":
-			if (icon) await drawIcon(ctx, 20, 20, 50, icon);
-			await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27);
-			await drawText(
-				ctx,
-				83,
-				58,
-				"#C9D2F0FF",
-				"thin 11px Verdana",
-				presence,
-			);
-			break;
-		case "banner4":
-			if (icon) await drawIcon(ctx, 21, 136, 50, icon);
-			await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27);
-			await drawText(
-				ctx,
-				84,
-				171,
-				"#C9D2F0FF",
-				"thin 12px Verdana",
-				presence,
-			);
-			break;
-		default:
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
+
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
+
+		// Fetch guild information
+		const icon = guild.icon;
+		const name = guild.name;
+		const presence = guild.presence_count + " ONLINE";
+
+		// Fetch parameter
+		const style = req.query.style?.toString() || "shield";
+		if (
+			!["shield", "banner1", "banner2", "banner3", "banner4"].includes(
+				style,
+			)
+		) {
 			throw new HTTPError(
 				"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
 				400,
 			);
-	}
-
-	// Return final image
-	const buffer = canvas.toBuffer("image/png");
-	res.set("Content-Type", "image/png");
-	res.set("Cache-Control", "public, max-age=3600");
-	return res.send(buffer);
-});
+		}
+
+		// Setup canvas
+		const { createCanvas } = require("canvas");
+		const { loadImage } = require("canvas");
+		const sizeOf = require("image-size");
+
+		// TODO: Widget style templates need Spacebar branding
+		const source = path.join(
+			__dirname,
+			"..",
+			"..",
+			"..",
+			"..",
+			"..",
+			"assets",
+			"widget",
+			`${style}.png`,
+		);
+		if (!fs.existsSync(source)) {
+			throw new HTTPError("Widget template does not exist.", 400);
+		}
+
+		// Create base template image for parameter
+		const { width, height } = await sizeOf(source);
+		const canvas = createCanvas(width, height);
+		const ctx = canvas.getContext("2d");
+		const template = await loadImage(source);
+		ctx.drawImage(template, 0, 0);
+
+		// Add the guild specific information to the template asset image
+		switch (style) {
+			case "shield":
+				ctx.textAlign = "center";
+				await drawText(
+					ctx,
+					73,
+					13,
+					"#FFFFFF",
+					"thin 10px Verdana",
+					presence,
+				);
+				break;
+			case "banner1":
+				if (icon) await drawIcon(ctx, 20, 27, 50, icon);
+				await drawText(
+					ctx,
+					83,
+					51,
+					"#FFFFFF",
+					"12px Verdana",
+					name,
+					22,
+				);
+				await drawText(
+					ctx,
+					83,
+					66,
+					"#C9D2F0FF",
+					"thin 11px Verdana",
+					presence,
+				);
+				break;
+			case "banner2":
+				if (icon) await drawIcon(ctx, 13, 19, 36, icon);
+				await drawText(
+					ctx,
+					62,
+					34,
+					"#FFFFFF",
+					"12px Verdana",
+					name,
+					15,
+				);
+				await drawText(
+					ctx,
+					62,
+					49,
+					"#C9D2F0FF",
+					"thin 11px Verdana",
+					presence,
+				);
+				break;
+			case "banner3":
+				if (icon) await drawIcon(ctx, 20, 20, 50, icon);
+				await drawText(
+					ctx,
+					83,
+					44,
+					"#FFFFFF",
+					"12px Verdana",
+					name,
+					27,
+				);
+				await drawText(
+					ctx,
+					83,
+					58,
+					"#C9D2F0FF",
+					"thin 11px Verdana",
+					presence,
+				);
+				break;
+			case "banner4":
+				if (icon) await drawIcon(ctx, 21, 136, 50, icon);
+				await drawText(
+					ctx,
+					84,
+					156,
+					"#FFFFFF",
+					"13px Verdana",
+					name,
+					27,
+				);
+				await drawText(
+					ctx,
+					84,
+					171,
+					"#C9D2F0FF",
+					"thin 12px Verdana",
+					presence,
+				);
+				break;
+			default:
+				throw new HTTPError(
+					"Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').",
+					400,
+				);
+		}
+
+		// Return final image
+		const buffer = canvas.toBuffer("image/png");
+		res.set("Content-Type", "image/png");
+		res.set("Cache-Control", "public, max-age=3600");
+		return res.send(buffer);
+	},
+);
 
 async function drawIcon(
 	canvas: any,
diff --git a/src/api/routes/guilds/#guild_id/widget.ts b/src/api/routes/guilds/#guild_id/widget.ts
index 77af25dc..cae0d6be 100644
--- a/src/api/routes/guilds/#guild_id/widget.ts
+++ b/src/api/routes/guilds/#guild_id/widget.ts
@@ -16,28 +16,55 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
-import { Guild, WidgetModifySchema } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { Guild, WidgetModifySchema } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 // https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { guild_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "GuildWidgetSettingsResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { guild_id } = req.params;
 
-	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
 
-	return res.json({
-		enabled: guild.widget_enabled || false,
-		channel_id: guild.widget_channel_id || null,
-	});
-});
+		return res.json({
+			enabled: guild.widget_enabled || false,
+			channel_id: guild.widget_channel_id || null,
+		});
+	},
+);
 
 // https://discord.com/developers/docs/resources/guild#modify-guild-widget
 router.patch(
 	"/",
-	route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }),
+	route({
+		requestBody: "WidgetModifySchema",
+		permission: "MANAGE_GUILD",
+		responses: {
+			200: {
+				body: "WidgetModifySchema",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as WidgetModifySchema;
 		const { guild_id } = req.params;
diff --git a/src/api/routes/guilds/index.ts b/src/api/routes/guilds/index.ts
index c793d185..545beb18 100644
--- a/src/api/routes/guilds/index.ts
+++ b/src/api/routes/guilds/index.ts
@@ -16,16 +16,16 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
-	Guild,
 	Config,
-	getRights,
-	Member,
 	DiscordApiErrors,
+	Guild,
 	GuildCreateSchema,
+	Member,
+	getRights,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
@@ -33,7 +33,21 @@ const router: Router = Router();
 
 router.post(
 	"/",
-	route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }),
+	route({
+		requestBody: "GuildCreateSchema",
+		right: "CREATE_GUILDS",
+		responses: {
+			201: {
+				body: "GuildCreateResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as GuildCreateSchema;
 
@@ -58,7 +72,7 @@ router.post(
 
 		await Member.addToGuild(req.user_id, guild.id);
 
-		res.status(201).json({ id: guild.id });
+		res.status(201).json(guild);
 	},
 );
 
diff --git a/src/api/routes/guilds/templates/index.ts b/src/api/routes/guilds/templates/index.ts
index bfbb7d3b..8f718a21 100644
--- a/src/api/routes/guilds/templates/index.ts
+++ b/src/api/routes/guilds/templates/index.ts
@@ -16,72 +16,91 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
-	Template,
+	Config,
+	DiscordApiErrors,
 	Guild,
+	GuildTemplateCreateSchema,
+	Member,
 	Role,
 	Snowflake,
-	Config,
-	Member,
-	GuildTemplateCreateSchema,
+	Template,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
-import { DiscordApiErrors } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import fetch from "node-fetch";
 const router: Router = Router();
 
-router.get("/:code", route({}), async (req: Request, res: Response) => {
-	const { allowDiscordTemplates, allowRaws, enabled } =
-		Config.get().templates;
-	if (!enabled)
-		res.json({
-			code: 403,
-			message: "Template creation & usage is disabled on this instance.",
-		}).sendStatus(403);
-
-	const { code } = req.params;
+router.get(
+	"/:code",
+	route({
+		responses: {
+			200: {
+				body: "Template",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { allowDiscordTemplates, allowRaws, enabled } =
+			Config.get().templates;
+		if (!enabled)
+			res.json({
+				code: 403,
+				message:
+					"Template creation & usage is disabled on this instance.",
+			}).sendStatus(403);
 
-	if (code.startsWith("discord:")) {
-		if (!allowDiscordTemplates)
-			return res
-				.json({
-					code: 403,
-					message:
-						"Discord templates cannot be used on this instance.",
-				})
-				.sendStatus(403);
-		const discordTemplateID = code.split("discord:", 2)[1];
+		const { code } = req.params;
 
-		const discordTemplateData = await fetch(
-			`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`,
-			{
-				method: "get",
-				headers: { "Content-Type": "application/json" },
-			},
-		);
-		return res.json(await discordTemplateData.json());
-	}
+		if (code.startsWith("discord:")) {
+			if (!allowDiscordTemplates)
+				return res
+					.json({
+						code: 403,
+						message:
+							"Discord templates cannot be used on this instance.",
+					})
+					.sendStatus(403);
+			const discordTemplateID = code.split("discord:", 2)[1];
+
+			const discordTemplateData = await fetch(
+				`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`,
+				{
+					method: "get",
+					headers: { "Content-Type": "application/json" },
+				},
+			);
+			return res.json(await discordTemplateData.json());
+		}
 
-	if (code.startsWith("external:")) {
-		if (!allowRaws)
-			return res
-				.json({
-					code: 403,
-					message: "Importing raws is disabled on this instance.",
-				})
-				.sendStatus(403);
+		if (code.startsWith("external:")) {
+			if (!allowRaws)
+				return res
+					.json({
+						code: 403,
+						message: "Importing raws is disabled on this instance.",
+					})
+					.sendStatus(403);
 
-		return res.json(code.split("external:", 2)[1]);
-	}
+			return res.json(code.split("external:", 2)[1]);
+		}
 
-	const template = await Template.findOneOrFail({ where: { code: code } });
-	res.json(template);
-});
+		const template = await Template.findOneOrFail({
+			where: { code: code },
+		});
+		res.json(template);
+	},
+);
 
 router.post(
 	"/:code",
-	route({ body: "GuildTemplateCreateSchema" }),
+	route({ requestBody: "GuildTemplateCreateSchema" }),
 	async (req: Request, res: Response) => {
 		const {
 			enabled,
diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts
index 6680e375..6de4d583 100644
--- a/src/api/routes/invites/index.ts
+++ b/src/api/routes/invites/index.ts
@@ -16,36 +16,68 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
+	DiscordApiErrors,
 	emitEvent,
 	getPermission,
 	Guild,
 	Invite,
 	InviteDeleteEvent,
-	User,
 	PublicInviteRelation,
+	User,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 const router: Router = Router();
 
-router.get("/:code", route({}), async (req: Request, res: Response) => {
-	const { code } = req.params;
+router.get(
+	"/:code",
+	route({
+		responses: {
+			"200": {
+				body: "Invite",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { code } = req.params;
 
-	const invite = await Invite.findOneOrFail({
-		where: { code },
-		relations: PublicInviteRelation,
-	});
+		const invite = await Invite.findOneOrFail({
+			where: { code },
+			relations: PublicInviteRelation,
+		});
 
-	res.status(200).send(invite);
-});
+		res.status(200).send(invite);
+	},
+);
 
 router.post(
 	"/:code",
-	route({ right: "USE_MASS_INVITES" }),
+	route({
+		right: "USE_MASS_INVITES",
+		responses: {
+			"200": {
+				body: "Invite",
+			},
+			401: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
+		if (req.user_bot) throw DiscordApiErrors.BOT_PROHIBITED_ENDPOINT;
+
 		const { code } = req.params;
 		const { guild_id } = await Invite.findOneOrFail({
 			where: { code: code },
@@ -75,33 +107,56 @@ router.post(
 );
 
 // * cant use permission of route() function because path doesn't have guild_id/channel_id
-router.delete("/:code", route({}), async (req: Request, res: Response) => {
-	const { code } = req.params;
-	const invite = await Invite.findOneOrFail({ where: { code } });
-	const { guild_id, channel_id } = invite;
-
-	const permission = await getPermission(req.user_id, guild_id, channel_id);
+router.delete(
+	"/:code",
+	route({
+		responses: {
+			"200": {
+				body: "Invite",
+			},
+			401: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { code } = req.params;
+		const invite = await Invite.findOneOrFail({ where: { code } });
+		const { guild_id, channel_id } = invite;
 
-	if (!permission.has("MANAGE_GUILD") && !permission.has("MANAGE_CHANNELS"))
-		throw new HTTPError(
-			"You missing the MANAGE_GUILD or MANAGE_CHANNELS permission",
-			401,
+		const permission = await getPermission(
+			req.user_id,
+			guild_id,
+			channel_id,
 		);
 
-	await Promise.all([
-		Invite.delete({ code }),
-		emitEvent({
-			event: "INVITE_DELETE",
-			guild_id: guild_id,
-			data: {
-				channel_id: channel_id,
+		if (
+			!permission.has("MANAGE_GUILD") &&
+			!permission.has("MANAGE_CHANNELS")
+		)
+			throw new HTTPError(
+				"You missing the MANAGE_GUILD or MANAGE_CHANNELS permission",
+				401,
+			);
+
+		await Promise.all([
+			Invite.delete({ code }),
+			emitEvent({
+				event: "INVITE_DELETE",
 				guild_id: guild_id,
-				code: code,
-			},
-		} as InviteDeleteEvent),
-	]);
+				data: {
+					channel_id: channel_id,
+					guild_id: guild_id,
+					code: code,
+				},
+			} as InviteDeleteEvent),
+		]);
 
-	res.json({ invite: invite });
-});
+		res.json({ invite: invite });
+	},
+);
 
 export default router;
diff --git a/src/api/routes/oauth2/authorize.ts b/src/api/routes/oauth2/authorize.ts
index c041f671..7ae6fa84 100644
--- a/src/api/routes/oauth2/authorize.ts
+++ b/src/api/routes/oauth2/authorize.ts
@@ -16,126 +16,168 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import {
 	ApiError,
 	Application,
 	ApplicationAuthorizeSchema,
-	getPermission,
 	DiscordApiErrors,
 	Member,
 	Permissions,
 	User,
+	getPermission,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
 // TODO: scopes, other oauth types
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	// const { client_id, scope, response_type, redirect_url } = req.query;
-	const { client_id } = req.query;
-
-	const app = await Application.findOne({
-		where: {
-			id: client_id as string,
+router.get(
+	"/",
+	route({
+		responses: {
+			// TODO: I really didn't feel like typing all of it out
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
 		},
-		relations: ["bot"],
-	});
+	}),
+	async (req: Request, res: Response) => {
+		// const { client_id, scope, response_type, redirect_url } = req.query;
+		const { client_id } = req.query;
 
-	// TODO: use DiscordApiErrors
-	// findOneOrFail throws code 404
-	if (!app) throw DiscordApiErrors.UNKNOWN_APPLICATION;
-	if (!app.bot) throw DiscordApiErrors.OAUTH2_APPLICATION_BOT_ABSENT;
+		const app = await Application.findOne({
+			where: {
+				id: client_id as string,
+			},
+			relations: ["bot"],
+		});
 
-	const bot = app.bot;
-	delete app.bot;
+		// TODO: use DiscordApiErrors
+		// findOneOrFail throws code 404
+		if (!app) throw DiscordApiErrors.UNKNOWN_APPLICATION;
+		if (!app.bot) throw DiscordApiErrors.OAUTH2_APPLICATION_BOT_ABSENT;
 
-	const user = await User.findOneOrFail({
-		where: {
-			id: req.user_id,
-			bot: false,
-		},
-		select: ["id", "username", "avatar", "discriminator", "public_flags"],
-	});
+		const bot = app.bot;
+		delete app.bot;
 
-	const guilds = await Member.find({
-		where: {
-			user: {
+		const user = await User.findOneOrFail({
+			where: {
 				id: req.user_id,
+				bot: false,
 			},
-		},
-		relations: ["guild", "roles"],
-		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-		//@ts-ignore
-		// prettier-ignore
-		select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id"],
-	});
-
-	const guildsWithPermissions = guilds.map((x) => {
-		const perms =
-			x.guild.owner_id === user.id
-				? new Permissions(Permissions.FLAGS.ADMINISTRATOR)
-				: Permissions.finalPermission({
-						user: {
-							id: user.id,
-							roles: x.roles?.map((x) => x.id) || [],
-						},
-						guild: {
-							roles: x?.roles || [],
-						},
-				  });
-
-		return {
-			id: x.guild.id,
-			name: x.guild.name,
-			icon: x.guild.icon,
-			mfa_level: x.guild.mfa_level,
-			permissions: perms.bitfield.toString(),
-		};
-	});
-
-	return res.json({
-		guilds: guildsWithPermissions,
-		user: {
-			id: user.id,
-			username: user.username,
-			avatar: user.avatar,
-			avatar_decoration: null, // TODO
-			discriminator: user.discriminator,
-			public_flags: user.public_flags,
-		},
-		application: {
-			id: app.id,
-			name: app.name,
-			icon: app.icon,
-			description: app.description,
-			summary: app.summary,
-			type: app.type,
-			hook: app.hook,
-			guild_id: null, // TODO support guilds
-			bot_public: app.bot_public,
-			bot_require_code_grant: app.bot_require_code_grant,
-			verify_key: app.verify_key,
-			flags: app.flags,
-		},
-		bot: {
-			id: bot.id,
-			username: bot.username,
-			avatar: bot.avatar,
-			avatar_decoration: null, // TODO
-			discriminator: bot.discriminator,
-			public_flags: bot.public_flags,
-			bot: true,
-			approximated_guild_count: 0, // TODO
-		},
-		authorized: false,
-	});
-});
+			select: [
+				"id",
+				"username",
+				"avatar",
+				"discriminator",
+				"public_flags",
+			],
+		});
+
+		const guilds = await Member.find({
+			where: {
+				user: {
+					id: req.user_id,
+				},
+			},
+			relations: ["guild", "roles"],
+			// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+			//@ts-ignore
+			// prettier-ignore
+			select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id"],
+		});
+
+		const guildsWithPermissions = guilds.map((x) => {
+			const perms =
+				x.guild.owner_id === user.id
+					? new Permissions(Permissions.FLAGS.ADMINISTRATOR)
+					: Permissions.finalPermission({
+							user: {
+								id: user.id,
+								roles: x.roles?.map((x) => x.id) || [],
+							},
+							guild: {
+								roles: x?.roles || [],
+							},
+					  });
+
+			return {
+				id: x.guild.id,
+				name: x.guild.name,
+				icon: x.guild.icon,
+				mfa_level: x.guild.mfa_level,
+				permissions: perms.bitfield.toString(),
+			};
+		});
+
+		return res.json({
+			guilds: guildsWithPermissions,
+			user: {
+				id: user.id,
+				username: user.username,
+				avatar: user.avatar,
+				avatar_decoration: null, // TODO
+				discriminator: user.discriminator,
+				public_flags: user.public_flags,
+			},
+			application: {
+				id: app.id,
+				name: app.name,
+				icon: app.icon,
+				description: app.description,
+				summary: app.summary,
+				type: app.type,
+				hook: app.hook,
+				guild_id: null, // TODO support guilds
+				bot_public: app.bot_public,
+				bot_require_code_grant: app.bot_require_code_grant,
+				verify_key: app.verify_key,
+				flags: app.flags,
+			},
+			bot: {
+				id: bot.id,
+				username: bot.username,
+				avatar: bot.avatar,
+				avatar_decoration: null, // TODO
+				discriminator: bot.discriminator,
+				public_flags: bot.public_flags,
+				bot: true,
+				approximated_guild_count: 0, // TODO
+			},
+			authorized: false,
+		});
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "ApplicationAuthorizeSchema" }),
+	route({
+		requestBody: "ApplicationAuthorizeSchema",
+		query: {
+			client_id: {
+				type: "string",
+			},
+		},
+		responses: {
+			200: {
+				body: "OAuthAuthorizeResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as ApplicationAuthorizeSchema;
 		// const { client_id, scope, response_type, redirect_url } = req.query;
diff --git a/src/api/routes/ping.ts b/src/api/routes/ping.ts
index 0fb6d9d0..73330239 100644
--- a/src/api/routes/ping.ts
+++ b/src/api/routes/ping.ts
@@ -16,29 +16,39 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), (req: Request, res: Response) => {
-	const { general } = Config.get();
-	res.send({
-		ping: "pong!",
-		instance: {
-			id: general.instanceId,
-			name: general.instanceName,
-			description: general.instanceDescription,
-			image: general.image,
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "InstancePingResponse",
+			},
+		},
+	}),
+	(req: Request, res: Response) => {
+		const { general } = Config.get();
+		res.send({
+			ping: "pong!",
+			instance: {
+				id: general.instanceId,
+				name: general.instanceName,
+				description: general.instanceDescription,
+				image: general.image,
 
-			correspondenceEmail: general.correspondenceEmail,
-			correspondenceUserID: general.correspondenceUserID,
+				correspondenceEmail: general.correspondenceEmail,
+				correspondenceUserID: general.correspondenceUserID,
 
-			frontPage: general.frontPage,
-			tosPage: general.tosPage,
-		},
-	});
-});
+				frontPage: general.frontPage,
+				tosPage: general.tosPage,
+			},
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/policies/instance/domains.ts b/src/api/routes/policies/instance/domains.ts
index 696a8510..afeb0e85 100644
--- a/src/api/routes/policies/instance/domains.ts
+++ b/src/api/routes/policies/instance/domains.ts
@@ -16,25 +16,38 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { cdn, gateway, api } = Config.get();
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "InstanceDomainsResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { cdn, gateway, api } = Config.get();
 
-	const IdentityForm = {
-		cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001",
-		gateway:
-			gateway.endpointPublic ||
-			process.env.GATEWAY ||
-			"ws://localhost:3001",
-		defaultApiVersion: api.defaultVersion ?? 9,
-		apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/",
-	};
+		const IdentityForm = {
+			cdn:
+				cdn.endpointPublic ||
+				process.env.CDN ||
+				"http://localhost:3001",
+			gateway:
+				gateway.endpointPublic ||
+				process.env.GATEWAY ||
+				"ws://localhost:3001",
+			defaultApiVersion: api.defaultVersion ?? 9,
+			apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/",
+		};
 
-	res.json(IdentityForm);
-});
+		res.json(IdentityForm);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/policies/instance/index.ts b/src/api/routes/policies/instance/index.ts
index 68ce3b42..6e269a5c 100644
--- a/src/api/routes/policies/instance/index.ts
+++ b/src/api/routes/policies/instance/index.ts
@@ -16,14 +16,24 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { general } = Config.get();
-	res.json(general);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIGeneralConfiguration",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { general } = Config.get();
+		res.json(general);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/policies/instance/limits.ts b/src/api/routes/policies/instance/limits.ts
index a6f13170..9852459d 100644
--- a/src/api/routes/policies/instance/limits.ts
+++ b/src/api/routes/policies/instance/limits.ts
@@ -16,14 +16,24 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import { Config } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { limits } = Config.get();
-	res.json(limits);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APILimitsConfiguration",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { limits } = Config.get();
+		res.json(limits);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/policies/stats.ts b/src/api/routes/policies/stats.ts
index 3939e1e8..b2cd3d5a 100644
--- a/src/api/routes/policies/stats.ts
+++ b/src/api/routes/policies/stats.ts
@@ -28,20 +28,33 @@ import {
 import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	if (!Config.get().security.statsWorldReadable) {
-		const rights = await getRights(req.user_id);
-		rights.hasThrow("VIEW_SERVER_STATS");
-	}
-
-	res.json({
-		counts: {
-			user: await User.count(),
-			guild: await Guild.count(),
-			message: await Message.count(),
-			members: await Member.count(),
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "InstanceStatsResponse",
+			},
+			403: {
+				body: "APIErrorResponse",
+			},
 		},
-	});
-});
+	}),
+	async (req: Request, res: Response) => {
+		if (!Config.get().security.statsWorldReadable) {
+			const rights = await getRights(req.user_id);
+			rights.hasThrow("VIEW_SERVER_STATS");
+		}
+
+		res.json({
+			counts: {
+				user: await User.count(),
+				guild: await Guild.count(),
+				message: await Message.count(),
+				members: await Member.count(),
+			},
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/read-states/ack-bulk.ts b/src/api/routes/read-states/ack-bulk.ts
index 2c51893b..3ee25d1a 100644
--- a/src/api/routes/read-states/ack-bulk.ts
+++ b/src/api/routes/read-states/ack-bulk.ts
@@ -16,14 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import { AckBulkSchema, ReadState } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "AckBulkSchema" }),
+	route({
+		requestBody: "AckBulkSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as AckBulkSchema;
 
diff --git a/src/api/routes/science.ts b/src/api/routes/science.ts
index 099da18b..d5cdc173 100644
--- a/src/api/routes/science.ts
+++ b/src/api/routes/science.ts
@@ -16,14 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.post("/", route({}), (req: Request, res: Response) => {
-	// TODO:
-	res.sendStatus(204);
-});
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+		},
+	}),
+	(req: Request, res: Response) => {
+		// TODO:
+		res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/sticker-packs/index.ts b/src/api/routes/sticker-packs/index.ts
index 234e03c6..569d1104 100644
--- a/src/api/routes/sticker-packs/index.ts
+++ b/src/api/routes/sticker-packs/index.ts
@@ -16,16 +16,28 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
 import { StickerPack } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const sticker_packs = await StickerPack.find({ relations: ["stickers"] });
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIStickerPackArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const sticker_packs = await StickerPack.find({
+			relations: ["stickers"],
+		});
 
-	res.json({ sticker_packs });
-});
+		res.json({ sticker_packs });
+	},
+);
 
 export default router;
diff --git a/src/api/routes/stickers/#sticker_id/index.ts b/src/api/routes/stickers/#sticker_id/index.ts
index 360149b5..2ea81bf9 100644
--- a/src/api/routes/stickers/#sticker_id/index.ts
+++ b/src/api/routes/stickers/#sticker_id/index.ts
@@ -16,15 +16,25 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Sticker } from "@spacebar/util";
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Sticker } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { sticker_id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "Sticker",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { sticker_id } = req.params;
 
-	res.json(await Sticker.find({ where: { id: sticker_id } }));
-});
+		res.json(await Sticker.find({ where: { id: sticker_id } }));
+	},
+);
 
 export default router;
diff --git a/src/api/routes/stop.ts b/src/api/routes/stop.ts
index 6a6e6277..79e132d7 100644
--- a/src/api/routes/stop.ts
+++ b/src/api/routes/stop.ts
@@ -16,14 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.post(
 	"/",
-	route({ right: "OPERATOR" }),
+	route({
+		right: "OPERATOR",
+		responses: {
+			200: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		console.log(`/stop was called by ${req.user_id} at ${new Date()}`);
 		res.sendStatus(200);
diff --git a/src/api/routes/updates.ts b/src/api/routes/updates.ts
index f7403899..101bd3bc 100644
--- a/src/api/routes/updates.ts
+++ b/src/api/routes/updates.ts
@@ -16,37 +16,53 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
 import { FieldErrors, Release } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const platform = req.query.platform;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "UpdatesResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const platform = req.query.platform;
 
-	if (!platform)
-		throw FieldErrors({
-			platform: {
-				code: "BASE_TYPE_REQUIRED",
-				message: req.t("common:field.BASE_TYPE_REQUIRED"),
+		if (!platform)
+			throw FieldErrors({
+				platform: {
+					code: "BASE_TYPE_REQUIRED",
+					message: req.t("common:field.BASE_TYPE_REQUIRED"),
+				},
+			});
+
+		const release = await Release.findOneOrFail({
+			where: {
+				enabled: true,
+				platform: platform as string,
 			},
+			order: { pub_date: "DESC" },
 		});
 
-	const release = await Release.findOneOrFail({
-		where: {
-			enabled: true,
-			platform: platform as string,
-		},
-		order: { pub_date: "DESC" },
-	});
-
-	res.json({
-		name: release.name,
-		pub_date: release.pub_date,
-		url: release.url,
-		notes: release.notes,
-	});
-});
+		res.json({
+			name: release.name,
+			pub_date: release.pub_date,
+			url: release.url,
+			notes: release.notes,
+		});
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/#id/delete.ts b/src/api/routes/users/#id/delete.ts
index e36a35e6..5b1a682c 100644
--- a/src/api/routes/users/#id/delete.ts
+++ b/src/api/routes/users/#id/delete.ts
@@ -30,7 +30,18 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ right: "MANAGE_USERS" }),
+	route({
+		right: "MANAGE_USERS",
+		responses: {
+			204: {},
+			403: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		await User.findOneOrFail({
 			where: { id: req.params.id },
diff --git a/src/api/routes/users/#id/index.ts b/src/api/routes/users/#id/index.ts
index 0c7cfe37..1bd413d3 100644
--- a/src/api/routes/users/#id/index.ts
+++ b/src/api/routes/users/#id/index.ts
@@ -16,16 +16,26 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { User } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { User } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const { id } = req.params;
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIPublicUser",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { id } = req.params;
 
-	res.json(await User.getPublicUser(id));
-});
+		res.json(await User.getPublicUser(id));
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts
index 2836c563..eecec0f3 100644
--- a/src/api/routes/users/#id/profile.ts
+++ b/src/api/routes/users/#id/profile.ts
@@ -16,23 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
-	User,
 	Member,
-	UserProfileModifySchema,
-	handleFile,
 	PrivateUserProjection,
-	emitEvent,
+	User,
+	UserProfileModifySchema,
 	UserUpdateEvent,
+	emitEvent,
+	handleFile,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.get(
 	"/",
-	route({ test: { response: { body: "UserProfileResponse" } } }),
+	route({ responses: { 200: { body: "UserProfileResponse" } } }),
 	async (req: Request, res: Response) => {
 		if (req.params.id === "@me") req.params.id = req.user_id;
 
@@ -84,18 +84,6 @@ router.get(
 
 		// TODO: make proper DTO's in util?
 
-		const userDto = {
-			username: user.username,
-			discriminator: user.discriminator,
-			id: user.id,
-			public_flags: user.public_flags,
-			avatar: user.avatar,
-			accent_color: user.accent_color,
-			banner: user.banner,
-			bio: req.user_bot ? null : user.bio,
-			bot: user.bot,
-		};
-
 		const userProfile = {
 			bio: req.user_bot ? null : user.bio,
 			accent_color: user.accent_color,
@@ -104,28 +92,6 @@ router.get(
 			theme_colors: user.theme_colors,
 		};
 
-		const guildMemberDto = guild_member
-			? {
-					avatar: guild_member.avatar,
-					banner: guild_member.banner,
-					bio: req.user_bot ? null : guild_member.bio,
-					communication_disabled_until:
-						guild_member.communication_disabled_until,
-					deaf: guild_member.deaf,
-					flags: user.flags,
-					is_pending: guild_member.pending,
-					pending: guild_member.pending, // why is this here twice, discord?
-					joined_at: guild_member.joined_at,
-					mute: guild_member.mute,
-					nick: guild_member.nick,
-					premium_since: guild_member.premium_since,
-					roles: guild_member.roles
-						.map((x) => x.id)
-						.filter((id) => id != guild_id),
-					user: userDto,
-			  }
-			: undefined;
-
 		const guildMemberProfile = {
 			accent_color: null,
 			banner: guild_member?.banner || null,
@@ -139,11 +105,11 @@ router.get(
 			premium_guild_since: premium_guild_since, // TODO
 			premium_since: user.premium_since, // TODO
 			mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true
-			user: userDto,
+			user: user.toPublicUser(),
 			premium_type: user.premium_type,
 			profile_themes_experiment_bucket: 4, // TODO: This doesn't make it available, for some reason?
 			user_profile: userProfile,
-			guild_member: guild_id && guildMemberDto,
+			guild_member: guild_member?.toPublicMember(),
 			guild_member_profile: guild_id && guildMemberProfile,
 		});
 	},
@@ -151,7 +117,7 @@ router.get(
 
 router.patch(
 	"/",
-	route({ body: "UserProfileModifySchema" }),
+	route({ requestBody: "UserProfileModifySchema" }),
 	async (req: Request, res: Response) => {
 		const body = req.body as UserProfileModifySchema;
 
diff --git a/src/api/routes/users/#id/relationships.ts b/src/api/routes/users/#id/relationships.ts
index dfe52a5e..3737ca00 100644
--- a/src/api/routes/users/#id/relationships.ts
+++ b/src/api/routes/users/#id/relationships.ts
@@ -16,17 +16,25 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { User } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { User, UserRelationsResponse } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.get(
 	"/",
-	route({ test: { response: { body: "UserRelationsResponse" } } }),
+	route({
+		responses: {
+			200: { body: "UserRelationsResponse" },
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
-		const mutual_relations: object[] = [];
+		const mutual_relations: UserRelationsResponse = [];
+
 		const requested_relations = await User.findOneOrFail({
 			where: { id: req.params.id },
 			relations: ["relationships"],
diff --git a/src/api/routes/users/@me/channels.ts b/src/api/routes/users/@me/channels.ts
index 04db4fe9..8a8fadd9 100644
--- a/src/api/routes/users/@me/channels.ts
+++ b/src/api/routes/users/@me/channels.ts
@@ -16,32 +16,51 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
-	Recipient,
-	DmChannelDTO,
 	Channel,
 	DmChannelCreateSchema,
+	DmChannelDTO,
+	Recipient,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const recipients = await Recipient.find({
-		where: { user_id: req.user_id, closed: false },
-		relations: ["channel", "channel.recipients"],
-	});
-	res.json(
-		await Promise.all(
-			recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id])),
-		),
-	);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIDMChannelArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const recipients = await Recipient.find({
+			where: { user_id: req.user_id, closed: false },
+			relations: ["channel", "channel.recipients"],
+		});
+		res.json(
+			await Promise.all(
+				recipients.map((r) =>
+					DmChannelDTO.from(r.channel, [req.user_id]),
+				),
+			),
+		);
+	},
+);
 
 router.post(
 	"/",
-	route({ body: "DmChannelCreateSchema" }),
+	route({
+		requestBody: "DmChannelCreateSchema",
+		responses: {
+			200: {
+				body: "DmChannelDTO",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as DmChannelCreateSchema;
 		res.json(
diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts
index 3a4e5e0a..351ec99a 100644
--- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts
+++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts
@@ -29,7 +29,7 @@ const router = Router();
 // TODO: connection update schema
 router.patch(
 	"/",
-	route({ body: "ConnectionUpdateSchema" }),
+	route({ requestBody: "ConnectionUpdateSchema" }),
 	async (req: Request, res: Response) => {
 		const { connection_name, connection_id } = req.params;
 		const body = req.body as ConnectionUpdateSchema;
diff --git a/src/api/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts
index dce737fc..e36a1e92 100644
--- a/src/api/routes/users/@me/delete.ts
+++ b/src/api/routes/users/@me/delete.ts
@@ -16,41 +16,58 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { Member, User } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { Member, User } from "@spacebar/util";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 const router = Router();
 
-router.post("/", route({}), async (req: Request, res: Response) => {
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		select: ["data"],
-	}); //User object
-	let correctpass = true;
-
-	if (user.data.hash) {
-		// guest accounts can delete accounts without password
-		correctpass = await bcrypt.compare(req.body.password, user.data.hash);
-		if (!correctpass) {
-			throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+			401: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			select: ["data"],
+		}); //User object
+		let correctpass = true;
+
+		if (user.data.hash) {
+			// guest accounts can delete accounts without password
+			correctpass = await bcrypt.compare(
+				req.body.password,
+				user.data.hash,
+			);
+			if (!correctpass) {
+				throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
+			}
 		}
-	}
 
-	// TODO: decrement guild member count
+		// TODO: decrement guild member count
 
-	if (correctpass) {
-		await Promise.all([
-			User.delete({ id: req.user_id }),
-			Member.delete({ id: req.user_id }),
-		]);
+		if (correctpass) {
+			await Promise.all([
+				User.delete({ id: req.user_id }),
+				Member.delete({ id: req.user_id }),
+			]);
 
-		res.sendStatus(204);
-	} else {
-		res.sendStatus(401);
-	}
-});
+			res.sendStatus(204);
+		} else {
+			res.sendStatus(401);
+		}
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/disable.ts b/src/api/routes/users/@me/disable.ts
index d123a6a1..b4d03e62 100644
--- a/src/api/routes/users/@me/disable.ts
+++ b/src/api/routes/users/@me/disable.ts
@@ -16,35 +16,52 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { User } from "@spacebar/util";
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { User } from "@spacebar/util";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.post("/", route({}), async (req: Request, res: Response) => {
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		select: ["data"],
-	}); //User object
-	let correctpass = true;
+router.post(
+	"/",
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			select: ["data"],
+		}); //User object
+		let correctpass = true;
 
-	if (user.data.hash) {
-		// guest accounts can delete accounts without password
-		correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/
-	}
+		if (user.data.hash) {
+			// guest accounts can delete accounts without password
+			correctpass = await bcrypt.compare(
+				req.body.password,
+				user.data.hash,
+			); //Not sure if user typed right password :/
+		}
 
-	if (correctpass) {
-		await User.update({ id: req.user_id }, { disabled: true });
+		if (correctpass) {
+			await User.update({ id: req.user_id }, { disabled: true });
 
-		res.sendStatus(204);
-	} else {
-		res.status(400).json({
-			message: "Password does not match",
-			code: 50018,
-		});
-	}
-});
+			res.sendStatus(204);
+		} else {
+			res.status(400).json({
+				message: "Password does not match",
+				code: 50018,
+			});
+		}
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/guilds.ts b/src/api/routes/users/@me/guilds.ts
index b16b909d..0bce432b 100644
--- a/src/api/routes/users/@me/guilds.ts
+++ b/src/api/routes/users/@me/guilds.ts
@@ -16,79 +16,106 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
+	Config,
 	Guild,
-	Member,
-	User,
 	GuildDeleteEvent,
 	GuildMemberRemoveEvent,
+	Member,
+	User,
 	emitEvent,
-	Config,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const members = await Member.find({
-		relations: ["guild"],
-		where: { id: req.user_id },
-	});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIGuildArray",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const members = await Member.find({
+			relations: ["guild"],
+			where: { id: req.user_id },
+		});
 
-	let guild = members.map((x) => x.guild);
+		let guild = members.map((x) => x.guild);
 
-	if ("with_counts" in req.query && req.query.with_counts == "true") {
-		guild = []; // TODO: Load guilds with user role permissions number
-	}
+		if ("with_counts" in req.query && req.query.with_counts == "true") {
+			guild = []; // TODO: Load guilds with user role permissions number
+		}
 
-	res.json(guild);
-});
+		res.json(guild);
+	},
+);
 
 // user send to leave a certain guild
-router.delete("/:guild_id", route({}), async (req: Request, res: Response) => {
-	const { autoJoin } = Config.get().guild;
-	const { guild_id } = req.params;
-	const guild = await Guild.findOneOrFail({
-		where: { id: guild_id },
-		select: ["owner_id"],
-	});
+router.delete(
+	"/:guild_id",
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { autoJoin } = Config.get().guild;
+		const { guild_id } = req.params;
+		const guild = await Guild.findOneOrFail({
+			where: { id: guild_id },
+			select: ["owner_id"],
+		});
 
-	if (!guild) throw new HTTPError("Guild doesn't exist", 404);
-	if (guild.owner_id === req.user_id)
-		throw new HTTPError("You can't leave your own guild", 400);
-	if (
-		autoJoin.enabled &&
-		autoJoin.guilds.includes(guild_id) &&
-		!autoJoin.canLeave
-	) {
-		throw new HTTPError("You can't leave instance auto join guilds", 400);
-	}
+		if (!guild) throw new HTTPError("Guild doesn't exist", 404);
+		if (guild.owner_id === req.user_id)
+			throw new HTTPError("You can't leave your own guild", 400);
+		if (
+			autoJoin.enabled &&
+			autoJoin.guilds.includes(guild_id) &&
+			!autoJoin.canLeave
+		) {
+			throw new HTTPError(
+				"You can't leave instance auto join guilds",
+				400,
+			);
+		}
 
-	await Promise.all([
-		Member.delete({ id: req.user_id, guild_id: guild_id }),
-		emitEvent({
-			event: "GUILD_DELETE",
-			data: {
-				id: guild_id,
-			},
-			user_id: req.user_id,
-		} as GuildDeleteEvent),
-	]);
+		await Promise.all([
+			Member.delete({ id: req.user_id, guild_id: guild_id }),
+			emitEvent({
+				event: "GUILD_DELETE",
+				data: {
+					id: guild_id,
+				},
+				user_id: req.user_id,
+			} as GuildDeleteEvent),
+		]);
 
-	const user = await User.getPublicUser(req.user_id);
+		const user = await User.getPublicUser(req.user_id);
 
-	await emitEvent({
-		event: "GUILD_MEMBER_REMOVE",
-		data: {
+		await emitEvent({
+			event: "GUILD_MEMBER_REMOVE",
+			data: {
+				guild_id: guild_id,
+				user: user,
+			},
 			guild_id: guild_id,
-			user: user,
-		},
-		guild_id: guild_id,
-	} as GuildMemberRemoveEvent);
+		} as GuildMemberRemoveEvent);
 
-	return res.sendStatus(204);
-});
+		return res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/guilds/#guild_id/settings.ts b/src/api/routes/users/@me/guilds/#guild_id/settings.ts
index 7e9f2a08..ac6586ce 100644
--- a/src/api/routes/users/@me/guilds/#guild_id/settings.ts
+++ b/src/api/routes/users/@me/guilds/#guild_id/settings.ts
@@ -16,29 +16,49 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	Member,
 	OrmUtils,
 	UserGuildSettingsSchema,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
 // GET doesn't exist on discord.com
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const user = await Member.findOneOrFail({
-		where: { id: req.user_id, guild_id: req.params.guild_id },
-		select: ["settings"],
-	});
-	return res.json(user.settings);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await Member.findOneOrFail({
+			where: { id: req.user_id, guild_id: req.params.guild_id },
+			select: ["settings"],
+		});
+		return res.json(user.settings);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "UserGuildSettingsSchema" }),
+	route({
+		requestBody: "UserGuildSettingsSchema",
+		responses: {
+			200: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as UserGuildSettingsSchema;
 
diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts
index b3eeb964..8fe86265 100644
--- a/src/api/routes/users/@me/index.ts
+++ b/src/api/routes/users/@me/index.ts
@@ -16,36 +16,59 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
-	User,
-	PrivateUserProjection,
-	emitEvent,
-	UserUpdateEvent,
-	handleFile,
-	FieldErrors,
 	adjustEmail,
 	Config,
-	UserModifySchema,
+	emitEvent,
+	FieldErrors,
 	generateToken,
+	handleFile,
+	PrivateUserProjection,
+	User,
+	UserModifySchema,
+	UserUpdateEvent,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	res.json(
-		await User.findOne({
-			select: PrivateUserProjection,
-			where: { id: req.user_id },
-		}),
-	);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIPrivateUser",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		res.json(
+			await User.findOne({
+				select: PrivateUserProjection,
+				where: { id: req.user_id },
+			}),
+		);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "UserModifySchema" }),
+	route({
+		requestBody: "UserModifySchema",
+		responses: {
+			200: {
+				body: "UserUpdateResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as UserModifySchema;
 
diff --git a/src/api/routes/users/@me/mfa/codes-verification.ts b/src/api/routes/users/@me/mfa/codes-verification.ts
index 69d45e91..f71704a9 100644
--- a/src/api/routes/users/@me/mfa/codes-verification.ts
+++ b/src/api/routes/users/@me/mfa/codes-verification.ts
@@ -16,21 +16,34 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import {
 	BackupCode,
-	generateMfaBackupCodes,
-	User,
 	CodesVerificationSchema,
 	DiscordApiErrors,
+	User,
+	generateMfaBackupCodes,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "CodesVerificationSchema" }),
+	route({
+		requestBody: "CodesVerificationSchema",
+		responses: {
+			200: {
+				body: "APIBackupCodeArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		// const { key, nonce, regenerate } = req.body as CodesVerificationSchema;
 		const { regenerate } = req.body as CodesVerificationSchema;
diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts
index 4ddbf78e..f9cfc4c4 100644
--- a/src/api/routes/users/@me/mfa/codes.ts
+++ b/src/api/routes/users/@me/mfa/codes.ts
@@ -16,16 +16,16 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
 import {
 	BackupCode,
 	FieldErrors,
 	generateMfaBackupCodes,
-	User,
 	MfaCodesSchema,
+	User,
 } from "@spacebar/util";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
@@ -33,7 +33,23 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "MfaCodesSchema" }),
+	route({
+		requestBody: "MfaCodesSchema",
+		deprecated: true,
+		description:
+			"This route is replaced with users/@me/mfa/codes-verification in newer clients",
+		responses: {
+			200: {
+				body: "APIBackupCodeArray",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { password, regenerate } = req.body as MfaCodesSchema;
 
diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts
index 9f406423..362152d7 100644
--- a/src/api/routes/users/@me/mfa/totp/disable.ts
+++ b/src/api/routes/users/@me/mfa/totp/disable.ts
@@ -16,22 +16,32 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
 import { route } from "@spacebar/api";
-import { verifyToken } from "node-2fa";
-import { HTTPError } from "lambert-server";
 import {
-	User,
-	generateToken,
 	BackupCode,
 	TotpDisableSchema,
+	User,
+	generateToken,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+import { verifyToken } from "node-2fa";
 
 const router = Router();
 
 router.post(
 	"/",
-	route({ body: "TotpDisableSchema" }),
+	route({
+		requestBody: "TotpDisableSchema",
+		responses: {
+			200: {
+				body: "TokenOnlyResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as TotpDisableSchema;
 
diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts
index 4d6b2763..19836e4d 100644
--- a/src/api/routes/users/@me/mfa/totp/enable.ts
+++ b/src/api/routes/users/@me/mfa/totp/enable.ts
@@ -16,15 +16,15 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
+import { route } from "@spacebar/api";
 import {
+	TotpEnableSchema,
 	User,
-	generateToken,
 	generateMfaBackupCodes,
-	TotpEnableSchema,
+	generateToken,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
 import bcrypt from "bcrypt";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { verifyToken } from "node-2fa";
 
@@ -32,7 +32,20 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "TotpEnableSchema" }),
+	route({
+		requestBody: "TotpEnableSchema",
+		responses: {
+			200: {
+				body: "TokenWithBackupCodesResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as TotpEnableSchema;
 
diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts
index 04aca7e4..9cf42def 100644
--- a/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts
+++ b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts
@@ -21,21 +21,31 @@ import { SecurityKey, User } from "@spacebar/util";
 import { Request, Response, Router } from "express";
 const router = Router();
 
-router.delete("/", route({}), async (req: Request, res: Response) => {
-	const { key_id } = req.params;
+router.delete(
+	"/",
+	route({
+		responses: {
+			204: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { key_id } = req.params;
 
-	await SecurityKey.delete({
-		id: key_id,
-		user_id: req.user_id,
-	});
+		await SecurityKey.delete({
+			id: key_id,
+			user_id: req.user_id,
+		});
 
-	const keys = await SecurityKey.count({ where: { user_id: req.user_id } });
+		const keys = await SecurityKey.count({
+			where: { user_id: req.user_id },
+		});
 
-	// disable webauthn if there are no keys left
-	if (keys === 0)
-		await User.update({ id: req.user_id }, { webauthn_enabled: false });
+		// disable webauthn if there are no keys left
+		if (keys === 0)
+			await User.update({ id: req.user_id }, { webauthn_enabled: false });
 
-	res.sendStatus(204);
-});
+		res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts
index 29dbb7cf..f383ffb7 100644
--- a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts
+++ b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts
@@ -73,7 +73,17 @@ router.get("/", route({}), async (req: Request, res: Response) => {
 
 router.post(
 	"/",
-	route({ body: "WebAuthnPostSchema" }),
+	route({
+		requestBody: "WebAuthnPostSchema",
+		responses: {
+			200: {
+				body: "WebAuthnCreateResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		if (!WebAuthn.fido2) {
 			// TODO: I did this for typescript and I can't use !
diff --git a/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts
index d05c799c..248e61f9 100644
--- a/src/api/routes/users/@me/notes.ts
+++ b/src/api/routes/users/@me/notes.ts
@@ -16,71 +16,99 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
 import { route } from "@spacebar/api";
-import { User, Note, emitEvent, Snowflake } from "@spacebar/util";
+import { Note, Snowflake, User, emitEvent } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/:id", route({}), async (req: Request, res: Response) => {
-	const { id } = req.params;
-
-	const note = await Note.findOneOrFail({
-		where: {
-			owner: { id: req.user_id },
-			target: { id: id },
+router.get(
+	"/:id",
+	route({
+		responses: {
+			200: {
+				body: "UserNoteResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
 		},
-	});
+	}),
+	async (req: Request, res: Response) => {
+		const { id } = req.params;
+
+		const note = await Note.findOneOrFail({
+			where: {
+				owner: { id: req.user_id },
+				target: { id: id },
+			},
+		});
 
-	return res.json({
-		note: note?.content,
-		note_user_id: id,
-		user_id: req.user_id,
-	});
-});
+		return res.json({
+			note: note?.content,
+			note_user_id: id,
+			user_id: req.user_id,
+		});
+	},
+);
 
-router.put("/:id", route({}), async (req: Request, res: Response) => {
-	const { id } = req.params;
-	const owner = await User.findOneOrFail({ where: { id: req.user_id } });
-	const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw
-	const { note } = req.body;
+router.put(
+	"/:id",
+	route({
+		requestBody: "UserNoteUpdateSchema",
+		responses: {
+			204: {},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { id } = req.params;
+		const owner = await User.findOneOrFail({ where: { id: req.user_id } });
+		const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw
+		const { note } = req.body;
 
-	if (note && note.length) {
-		// upsert a note
-		if (
-			await Note.findOne({
-				where: { owner: { id: owner.id }, target: { id: target.id } },
-			})
-		) {
-			Note.update(
-				{ owner: { id: owner.id }, target: { id: target.id } },
-				{ owner, target, content: note },
-			);
+		if (note && note.length) {
+			// upsert a note
+			if (
+				await Note.findOne({
+					where: {
+						owner: { id: owner.id },
+						target: { id: target.id },
+					},
+				})
+			) {
+				Note.update(
+					{ owner: { id: owner.id }, target: { id: target.id } },
+					{ owner, target, content: note },
+				);
+			} else {
+				Note.insert({
+					id: Snowflake.generate(),
+					owner,
+					target,
+					content: note,
+				});
+			}
 		} else {
-			Note.insert({
-				id: Snowflake.generate(),
-				owner,
-				target,
-				content: note,
+			await Note.delete({
+				owner: { id: owner.id },
+				target: { id: target.id },
 			});
 		}
-	} else {
-		await Note.delete({
-			owner: { id: owner.id },
-			target: { id: target.id },
-		});
-	}
 
-	await emitEvent({
-		event: "USER_NOTE_UPDATE",
-		data: {
-			note: note,
-			id: target.id,
-		},
-		user_id: owner.id,
-	});
+		await emitEvent({
+			event: "USER_NOTE_UPDATE",
+			data: {
+				note: note,
+				id: target.id,
+			},
+			user_id: owner.id,
+		});
 
-	return res.status(204);
-});
+		return res.status(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts
index e9ea47e6..bce0a654 100644
--- a/src/api/routes/users/@me/relationships.ts
+++ b/src/api/routes/users/@me/relationships.ts
@@ -16,20 +16,20 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
-	RelationshipAddEvent,
-	User,
+	Config,
+	DiscordApiErrors,
 	PublicUserProjection,
-	RelationshipType,
+	Relationship,
+	RelationshipAddEvent,
 	RelationshipRemoveEvent,
+	RelationshipType,
+	User,
 	emitEvent,
-	Relationship,
-	Config,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { DiscordApiErrors } from "@spacebar/util";
-import { route } from "@spacebar/api";
 
 const router = Router();
 
@@ -38,29 +38,53 @@ const userProjection: (keyof User)[] = [
 	...PublicUserProjection,
 ];
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		relations: ["relationships", "relationships.to"],
-		select: ["id", "relationships"],
-	});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "UserRelationshipsResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			relations: ["relationships", "relationships.to"],
+			select: ["id", "relationships"],
+		});
 
-	//TODO DTO
-	const related_users = user.relationships.map((r) => {
-		return {
-			id: r.to.id,
-			type: r.type,
-			nickname: null,
-			user: r.to.toPublicUser(),
-		};
-	});
+		//TODO DTO
+		const related_users = user.relationships.map((r) => {
+			return {
+				id: r.to.id,
+				type: r.type,
+				nickname: null,
+				user: r.to.toPublicUser(),
+			};
+		});
 
-	return res.json(related_users);
-});
+		return res.json(related_users);
+	},
+);
 
 router.put(
 	"/:id",
-	route({ body: "RelationshipPutSchema" }),
+	route({
+		requestBody: "RelationshipPutSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		return await updateRelationship(
 			req,
@@ -77,7 +101,18 @@ router.put(
 
 router.post(
 	"/",
-	route({ body: "RelationshipPostSchema" }),
+	route({
+		requestBody: "RelationshipPostSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		return await updateRelationship(
 			req,
@@ -98,64 +133,78 @@ router.post(
 	},
 );
 
-router.delete("/:id", route({}), async (req: Request, res: Response) => {
-	const { id } = req.params;
-	if (id === req.user_id)
-		throw new HTTPError("You can't remove yourself as a friend");
+router.delete(
+	"/:id",
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { id } = req.params;
+		if (id === req.user_id)
+			throw new HTTPError("You can't remove yourself as a friend");
 
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		select: userProjection,
-		relations: ["relationships"],
-	});
-	const friend = await User.findOneOrFail({
-		where: { id: id },
-		select: userProjection,
-		relations: ["relationships"],
-	});
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			select: userProjection,
+			relations: ["relationships"],
+		});
+		const friend = await User.findOneOrFail({
+			where: { id: id },
+			select: userProjection,
+			relations: ["relationships"],
+		});
 
-	const relationship = user.relationships.find((x) => x.to_id === id);
-	const friendRequest = friend.relationships.find(
-		(x) => x.to_id === req.user_id,
-	);
+		const relationship = user.relationships.find((x) => x.to_id === id);
+		const friendRequest = friend.relationships.find(
+			(x) => x.to_id === req.user_id,
+		);
 
-	if (!relationship)
-		throw new HTTPError("You are not friends with the user", 404);
-	if (relationship?.type === RelationshipType.blocked) {
-		// unblock user
+		if (!relationship)
+			throw new HTTPError("You are not friends with the user", 404);
+		if (relationship?.type === RelationshipType.blocked) {
+			// unblock user
+
+			await Promise.all([
+				Relationship.delete({ id: relationship.id }),
+				emitEvent({
+					event: "RELATIONSHIP_REMOVE",
+					user_id: req.user_id,
+					data: relationship.toPublicRelationship(),
+				} as RelationshipRemoveEvent),
+			]);
+			return res.sendStatus(204);
+		}
+		if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
+			await Promise.all([
+				Relationship.delete({ id: friendRequest.id }),
+				await emitEvent({
+					event: "RELATIONSHIP_REMOVE",
+					data: friendRequest.toPublicRelationship(),
+					user_id: id,
+				} as RelationshipRemoveEvent),
+			]);
+		}
 
 		await Promise.all([
 			Relationship.delete({ id: relationship.id }),
 			emitEvent({
 				event: "RELATIONSHIP_REMOVE",
-				user_id: req.user_id,
 				data: relationship.toPublicRelationship(),
+				user_id: req.user_id,
 			} as RelationshipRemoveEvent),
 		]);
-		return res.sendStatus(204);
-	}
-	if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
-		await Promise.all([
-			Relationship.delete({ id: friendRequest.id }),
-			await emitEvent({
-				event: "RELATIONSHIP_REMOVE",
-				data: friendRequest.toPublicRelationship(),
-				user_id: id,
-			} as RelationshipRemoveEvent),
-		]);
-	}
 
-	await Promise.all([
-		Relationship.delete({ id: relationship.id }),
-		emitEvent({
-			event: "RELATIONSHIP_REMOVE",
-			data: relationship.toPublicRelationship(),
-			user_id: req.user_id,
-		} as RelationshipRemoveEvent),
-	]);
-
-	return res.sendStatus(204);
-});
+		return res.sendStatus(204);
+	},
+);
 
 export default router;
 
diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts
index 62cfe904..d22d6de1 100644
--- a/src/api/routes/users/@me/settings.ts
+++ b/src/api/routes/users/@me/settings.ts
@@ -16,23 +16,49 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
-import { User, UserSettingsSchema } from "@spacebar/util";
 import { route } from "@spacebar/api";
+import { User, UserSettingsSchema } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const user = await User.findOneOrFail({
-		where: { id: req.user_id },
-		relations: ["settings"],
-	});
-	return res.json(user.settings);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "UserSettings",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const user = await User.findOneOrFail({
+			where: { id: req.user_id },
+			relations: ["settings"],
+		});
+		return res.json(user.settings);
+	},
+);
 
 router.patch(
 	"/",
-	route({ body: "UserSettingsSchema" }),
+	route({
+		requestBody: "UserSettingsSchema",
+		responses: {
+			200: {
+				body: "UserSettings",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const body = req.body as UserSettingsSchema;
 		if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale
diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts
index 59bac07f..10a8b21d 100644
--- a/src/api/routes/voice/regions.ts
+++ b/src/api/routes/voice/regions.ts
@@ -16,14 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { getIpAdress, route } from "@spacebar/api";
-import { getVoiceRegions } from "@spacebar/api";
+import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.get("/", route({}), async (req: Request, res: Response) => {
-	res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true?
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "APIGuildVoiceRegion",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true?
+	},
+);
 
 export default router;
diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts
index 604df4e9..5a0b48e6 100644
--- a/src/api/util/handlers/route.ts
+++ b/src/api/util/handlers/route.ts
@@ -17,21 +17,21 @@
 */
 
 import {
-	ajv,
 	DiscordApiErrors,
 	EVENT,
 	FieldErrors,
-	SpacebarApiErrors,
-	getPermission,
-	getRights,
-	normalizeBody,
 	PermissionResolvable,
 	Permissions,
 	RightResolvable,
 	Rights,
+	SpacebarApiErrors,
+	ajv,
+	getPermission,
+	getRights,
+	normalizeBody,
 } from "@spacebar/util";
-import { NextFunction, Request, Response } from "express";
 import { AnyValidateFunction } from "ajv/dist/core";
+import { NextFunction, Request, Response } from "express";
 
 declare global {
 	// TODO: fix this
@@ -52,21 +52,40 @@ export type RouteResponse = {
 export interface RouteOptions {
 	permission?: PermissionResolvable;
 	right?: RightResolvable;
-	body?: `${string}Schema`; // typescript interface name
-	test?: {
-		response?: RouteResponse;
-		body?: unknown;
-		path?: string;
-		event?: EVENT | EVENT[];
-		headers?: Record<string, string>;
+	requestBody?: `${string}Schema`; // typescript interface name
+	responses?: {
+		[status: number]: {
+			// body?: `${string}Response`;
+			body?: string;
+		};
+	};
+	event?: EVENT | EVENT[];
+	summary?: string;
+	description?: string;
+	query?: {
+		[key: string]: {
+			type: string;
+			required?: boolean;
+			description?: string;
+			values?: string[];
+		};
 	};
+	deprecated?: boolean;
+	// test?: {
+	// 	response?: RouteResponse;
+	// 	body?: unknown;
+	// 	path?: string;
+	// 	event?: EVENT | EVENT[];
+	// 	headers?: Record<string, string>;
+	// };
 }
 
 export function route(opts: RouteOptions) {
 	let validate: AnyValidateFunction | undefined;
-	if (opts.body) {
-		validate = ajv.getSchema(opts.body);
-		if (!validate) throw new Error(`Body schema ${opts.body} not found`);
+	if (opts.requestBody) {
+		validate = ajv.getSchema(opts.requestBody);
+		if (!validate)
+			throw new Error(`Body schema ${opts.requestBody} not found`);
 	}
 
 	return async (req: Request, res: Response, next: NextFunction) => {
diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts
index 172e9604..c51daf6c 100644
--- a/src/api/util/utility/ipAddress.ts
+++ b/src/api/util/utility/ipAddress.ts
@@ -102,7 +102,7 @@ export function getIpAdress(req: Request): string {
 	return (
 		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 		// @ts-ignore
-		req.headers[Config.get().security.forwadedFor] ||
+		req.headers[Config.get().security.forwardedFor] ||
 		req.socket.remoteAddress
 	);
 }
diff --git a/src/bundle/Server.ts b/src/bundle/Server.ts
index 6ba74be6..d281120d 100644
--- a/src/bundle/Server.ts
+++ b/src/bundle/Server.ts
@@ -51,7 +51,9 @@ async function main() {
 	await Config.init();
 	await Sentry.init(app);
 
-	server.listen(port);
+	await new Promise((resolve) =>
+		server.listen({ port }, () => resolve(undefined)),
+	);
 	await Promise.all([api.start(), cdn.start(), gateway.start()]);
 
 	Sentry.errorHandler(app);
diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts
index 68273ace..1991ebbe 100644
--- a/src/gateway/events/Connection.ts
+++ b/src/gateway/events/Connection.ts
@@ -45,7 +45,7 @@ export async function Connection(
 	socket: WebSocket,
 	request: IncomingMessage,
 ) {
-	const forwardedFor = Config.get().security.forwadedFor;
+	const forwardedFor = Config.get().security.forwardedFor;
 	const ipAddress = forwardedFor
 		? (request.headers[forwardedFor] as string)
 		: request.socket.remoteAddress;
diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts
index 64e50d92..77e1a25a 100644
--- a/src/gateway/opcodes/LazyRequest.ts
+++ b/src/gateway/opcodes/LazyRequest.ts
@@ -27,6 +27,8 @@ import {
 	User,
 	Presence,
 	partition,
+	Channel,
+	Permissions,
 } from "@spacebar/util";
 import {
 	WebSocket,
@@ -35,6 +37,7 @@ import {
 	OPCODES,
 	Send,
 } from "@spacebar/gateway";
+import murmur from "murmurhash-js/murmurhash3_gc";
 import { check } from "./instanceOf";
 
 // TODO: only show roles/members that have access to this channel
@@ -267,7 +270,31 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
 	if (!Array.isArray(ranges)) throw new Error("Not a valid Array");
 
 	const member_count = await Member.count({ where: { guild_id } });
-	const ops = await Promise.all(ranges.map((x) => getMembers(guild_id, x)));
+	const ops = await Promise.all(
+		ranges.map((x) => getMembers(guild_id, x as [number, number])),
+	);
+
+	let list_id = "everyone";
+
+	const channel = await Channel.findOneOrFail({
+		where: { id: channel_id },
+	});
+	if (channel.permission_overwrites) {
+		const perms: string[] = [];
+
+		channel.permission_overwrites.forEach((overwrite) => {
+			const { id, allow, deny } = overwrite;
+
+			if (allow.toBigInt() & Permissions.FLAGS.VIEW_CHANNEL)
+				perms.push(`allow:${id}`);
+			else if (deny.toBigInt() & Permissions.FLAGS.VIEW_CHANNEL)
+				perms.push(`deny:${id}`);
+		});
+
+		if (perms.length > 0) {
+			list_id = murmur(perms.sort().join(",")).toString();
+		}
+	}
 
 	// TODO: unsubscribe member_events that are not in op.members
 
@@ -297,7 +324,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
 				member_count -
 				(groups.find((x) => x.id == "offline")?.count ?? 0),
 			member_count,
-			id: "everyone",
+			id: list_id,
 			guild_id,
 			groups,
 		},
diff --git a/src/util/config/types/GeneralConfiguration.ts b/src/util/config/types/GeneralConfiguration.ts
index c20fe9a7..cff8c527 100644
--- a/src/util/config/types/GeneralConfiguration.ts
+++ b/src/util/config/types/GeneralConfiguration.ts
@@ -28,4 +28,5 @@ export class GeneralConfiguration {
 	correspondenceUserID: string | null = null;
 	image: string | null = null;
 	instanceId: string = Snowflake.generate();
+	autoCreateBotUsers: boolean = false;
 }
diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts
index 5e971cfe..35776642 100644
--- a/src/util/config/types/SecurityConfiguration.ts
+++ b/src/util/config/types/SecurityConfiguration.ts
@@ -28,7 +28,7 @@ export class SecurityConfiguration {
 	// header to get the real user ip address
 	// X-Forwarded-For for nginx/reverse proxies
 	// CF-Connecting-IP for cloudflare
-	forwadedFor: string | null = null;
+	forwardedFor: string | null = null;
 	ipdataApiKey: string | null =
 		"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9";
 	mfaBackupCodeCount: number = 10;
diff --git a/src/util/config/types/subconfigurations/limits/RateLimits.ts b/src/util/config/types/subconfigurations/limits/RateLimits.ts
index caba740b..0ce0827c 100644
--- a/src/util/config/types/subconfigurations/limits/RateLimits.ts
+++ b/src/util/config/types/subconfigurations/limits/RateLimits.ts
@@ -16,11 +16,11 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { RouteRateLimit, RateLimitOptions } from ".";
+import { RateLimitOptions, RouteRateLimit } from ".";
 
 export class RateLimits {
 	enabled: boolean = false;
-	ip: Omit<RateLimitOptions, "bot_count"> = {
+	ip: RateLimitOptions = {
 		count: 500,
 		window: 5,
 	};
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index 72490028..19a7a41a 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -486,6 +486,29 @@ export enum ChannelPermissionOverwriteType {
 export interface DMChannel extends Omit<Channel, "type" | "recipients"> {
 	type: ChannelType.DM | ChannelType.GROUP_DM;
 	recipients: Recipient[];
+}
 
-	// TODO: probably more props
+// TODO: probably more props
+export function isTextChannel(type: ChannelType): boolean {
+	switch (type) {
+		case ChannelType.GUILD_STORE:
+		case ChannelType.GUILD_VOICE:
+		case ChannelType.GUILD_STAGE_VOICE:
+		case ChannelType.GUILD_CATEGORY:
+		case ChannelType.GUILD_FORUM:
+		case ChannelType.DIRECTORY:
+			throw new HTTPError("not a text channel", 400);
+		case ChannelType.DM:
+		case ChannelType.GROUP_DM:
+		case ChannelType.GUILD_NEWS:
+		case ChannelType.GUILD_NEWS_THREAD:
+		case ChannelType.GUILD_PUBLIC_THREAD:
+		case ChannelType.GUILD_PRIVATE_THREAD:
+		case ChannelType.GUILD_TEXT:
+		case ChannelType.ENCRYPTED:
+		case ChannelType.ENCRYPTED_THREAD:
+			return true;
+		default:
+			throw new HTTPError("unimplemented", 400);
+	}
 }
diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
index 64dc2420..4c2949a3 100644
--- a/src/util/entities/Guild.ts
+++ b/src/util/entities/Guild.ts
@@ -24,7 +24,7 @@ import {
 	OneToMany,
 	RelationId,
 } from "typeorm";
-import { Config, handleFile, Snowflake } from "..";
+import { Config, GuildWelcomeScreen, handleFile, Snowflake } from "..";
 import { Ban } from "./Ban";
 import { BaseClass } from "./BaseClass";
 import { Channel } from "./Channel";
@@ -77,7 +77,7 @@ export class Guild extends BaseClass {
 	afk_channel?: Channel;
 
 	@Column({ nullable: true })
-	afk_timeout?: number = Config.get().defaults.guild.afkTimeout;
+	afk_timeout?: number;
 
 	// * commented out -> use owner instead
 	// application id of the guild creator if it is bot-created
@@ -95,8 +95,7 @@ export class Guild extends BaseClass {
 	banner?: string;
 
 	@Column({ nullable: true })
-	default_message_notifications?: number =
-		Config.get().defaults.guild.defaultMessageNotifications;
+	default_message_notifications?: number;
 
 	@Column({ nullable: true })
 	description?: string;
@@ -105,11 +104,10 @@ export class Guild extends BaseClass {
 	discovery_splash?: string;
 
 	@Column({ nullable: true })
-	explicit_content_filter?: number =
-		Config.get().defaults.guild.explicitContentFilter;
+	explicit_content_filter?: number;
 
 	@Column({ type: "simple-array" })
-	features: string[] = Config.get().guild.defaultFeatures || []; //TODO use enum
+	features: string[] = []; //TODO use enum
 	//TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features
 
 	@Column({ nullable: true })
@@ -122,14 +120,13 @@ export class Guild extends BaseClass {
 	large?: boolean = false;
 
 	@Column({ nullable: true })
-	max_members?: number = Config.get().limits.guild.maxMembers;
+	max_members?: number;
 
 	@Column({ nullable: true })
-	max_presences?: number = Config.get().defaults.guild.maxPresences;
+	max_presences?: number;
 
 	@Column({ nullable: true })
-	max_video_channel_users?: number =
-		Config.get().defaults.guild.maxVideoChannelUsers;
+	max_video_channel_users?: number;
 
 	@Column({ nullable: true })
 	member_count?: number;
@@ -247,7 +244,7 @@ export class Guild extends BaseClass {
 	rules_channel?: string;
 
 	@Column({ nullable: true })
-	region?: string = Config.get().regions.default;
+	region?: string;
 
 	@Column({ nullable: true })
 	splash?: string;
@@ -270,16 +267,7 @@ export class Guild extends BaseClass {
 	verification_level?: number;
 
 	@Column({ type: "simple-json" })
-	welcome_screen: {
-		enabled: boolean;
-		description: string;
-		welcome_channels: {
-			description: string;
-			emoji_id?: string;
-			emoji_name?: string;
-			channel_id: string;
-		}[];
-	};
+	welcome_screen: GuildWelcomeScreen;
 
 	@Column({ nullable: true })
 	@RelationId((guild: Guild) => guild.widget_channel)
@@ -336,6 +324,18 @@ export class Guild extends BaseClass {
 				description: "Fill in your description",
 				welcome_channels: [],
 			},
+
+			afk_timeout: Config.get().defaults.guild.afkTimeout,
+			default_message_notifications:
+				Config.get().defaults.guild.defaultMessageNotifications,
+			explicit_content_filter:
+				Config.get().defaults.guild.explicitContentFilter,
+			features: Config.get().guild.defaultFeatures,
+			max_members: Config.get().limits.guild.maxMembers,
+			max_presences: Config.get().defaults.guild.maxPresences,
+			max_video_channel_users:
+				Config.get().defaults.guild.maxVideoChannelUsers,
+			region: Config.get().regions.default,
 		}).save();
 
 		// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error
diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts
index 8c208202..8be6eae1 100644
--- a/src/util/entities/Member.ts
+++ b/src/util/entities/Member.ts
@@ -344,11 +344,7 @@ export class Member extends BaseClassWithoutId {
 				relations: ["user", "roles"],
 				take: 10,
 			})
-		).map((member) => ({
-			...member.toPublicMember(),
-			user: member.user.toPublicUser(),
-			roles: member.roles.map((x) => x.id),
-		}));
+		).map((member) => member.toPublicMember());
 
 		if (
 			await Member.count({
@@ -455,6 +451,10 @@ export class Member extends BaseClassWithoutId {
 		PublicMemberProjection.forEach((x) => {
 			member[x] = this[x];
 		});
+
+		if (member.roles) member.roles = member.roles.map((x: Role) => x.id);
+		if (member.user) member.user = member.user.toPublicUser();
+
 		return member as PublicMember;
 	}
 }
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index b30a7c58..68d7b5e8 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -26,11 +26,11 @@ import {
 	OneToOne,
 } from "typeorm";
 import {
-	adjustEmail,
 	Config,
 	Email,
 	FieldErrors,
 	Snowflake,
+	adjustEmail,
 	trimSpecial,
 } from "..";
 import { BitField } from "../util/BitField";
@@ -86,8 +86,7 @@ export const PrivateUserProjection = [
 
 // Private user data that should never get sent to the client
 export type PublicUser = Pick<User, PublicUserKeys>;
-
-export type UserPublic = Pick<User, PublicUserKeys>;
+export type PrivateUser = Pick<User, PrivateUserKeys>;
 
 export interface UserPrivate extends Pick<User, PrivateUserKeys> {
 	locale: string;
@@ -110,8 +109,10 @@ export class User extends BaseClass {
 	@Column({ nullable: true })
 	banner?: string; // hash of the user banner
 
+	// TODO: Separate `User` and `UserProfile` models
+	// puyo: changed from [number, number] because it breaks openapi
 	@Column({ nullable: true, type: "simple-array" })
-	theme_colors?: [number, number]; // TODO: Separate `User` and `UserProfile` models
+	theme_colors?: number[];
 
 	@Column({ nullable: true })
 	pronouns?: string;
@@ -126,10 +127,10 @@ export class User extends BaseClass {
 	mobile: boolean = false; // if the user has mobile app installed
 
 	@Column()
-	premium: boolean = Config.get().defaults.user.premium ?? false; // if user bought individual premium
+	premium: boolean; // if user bought individual premium
 
 	@Column()
-	premium_type: number = Config.get().defaults.user.premiumType ?? 0; // individual premium level
+	premium_type: number; // individual premium level
 
 	@Column()
 	bot: boolean = false; // if user is bot
@@ -156,13 +157,13 @@ export class User extends BaseClass {
 	totp_last_ticket?: string = "";
 
 	@Column()
-	created_at: Date = new Date(); // registration date
+	created_at: Date; // registration date
 
 	@Column({ nullable: true })
 	premium_since: Date; // premium date
 
 	@Column({ select: false })
-	verified: boolean = Config.get().defaults.user.verified ?? true; // email is verified
+	verified: boolean; // email is verified
 
 	@Column()
 	disabled: boolean = false; // if the account is disabled
@@ -390,11 +391,16 @@ export class User extends BaseClass {
 				valid_tokens_since: new Date(),
 			},
 			extended_settings: "{}",
+			settings: settings,
+
 			premium_since: Config.get().defaults.user.premium
 				? new Date()
 				: undefined,
-			settings: settings,
 			rights: Config.get().register.defaultRights,
+			premium: Config.get().defaults.user.premium ?? false,
+			premium_type: Config.get().defaults.user.premiumType ?? 0,
+			verified: Config.get().defaults.user.verified ?? true,
+			created_at: new Date(),
 		});
 
 		user.validate();
diff --git a/src/util/interfaces/Activity.ts b/src/util/interfaces/Activity.ts
index 7654ba90..0227f242 100644
--- a/src/util/interfaces/Activity.ts
+++ b/src/util/interfaces/Activity.ts
@@ -36,7 +36,7 @@ export interface Activity {
 	};
 	party?: {
 		id?: string;
-		size?: [number]; // used to show the party's current and maximum size // TODO: array length 2
+		size?: number[]; // used to show the party's current and maximum size // TODO: array length 2
 	};
 	assets?: {
 		large_image?: string; // the id for a large asset of the activity, usually a snowflake
diff --git a/src/util/interfaces/GuildWelcomeScreen.ts b/src/util/interfaces/GuildWelcomeScreen.ts
new file mode 100644
index 00000000..38b6061b
--- /dev/null
+++ b/src/util/interfaces/GuildWelcomeScreen.ts
@@ -0,0 +1,10 @@
+export interface GuildWelcomeScreen {
+	enabled: boolean;
+	description: string;
+	welcome_channels: {
+		description: string;
+		emoji_id?: string;
+		emoji_name?: string;
+		channel_id: string;
+	}[];
+}
diff --git a/src/util/interfaces/index.ts b/src/util/interfaces/index.ts
index c6a00458..6620ba32 100644
--- a/src/util/interfaces/index.ts
+++ b/src/util/interfaces/index.ts
@@ -19,6 +19,7 @@
 export * from "./Activity";
 export * from "./ConnectedAccount";
 export * from "./Event";
+export * from "./GuildWelcomeScreen";
 export * from "./Interaction";
 export * from "./Presence";
 export * from "./Status";
diff --git a/src/util/schemas/AckBulkSchema.ts b/src/util/schemas/AckBulkSchema.ts
index cf6dc597..5604c2fc 100644
--- a/src/util/schemas/AckBulkSchema.ts
+++ b/src/util/schemas/AckBulkSchema.ts
@@ -17,11 +17,9 @@
 */
 
 export interface AckBulkSchema {
-	read_states: [
-		{
-			channel_id: string;
-			message_id: string;
-			read_state_type: number; // WHat is this?
-		},
-	];
+	read_states: {
+		channel_id: string;
+		message_id: string;
+		read_state_type: number; // WHat is this?
+	}[];
 }
diff --git a/src/util/schemas/IdentifySchema.ts b/src/util/schemas/IdentifySchema.ts
index fb48c2a4..0619dacc 100644
--- a/src/util/schemas/IdentifySchema.ts
+++ b/src/util/schemas/IdentifySchema.ts
@@ -66,6 +66,7 @@ export const IdentifySchema = {
 		$private_channels_version: Number,
 		$guild_versions: Object,
 		$api_code_version: Number,
+		$initial_guild_id: String,
 	},
 	$clientState: {
 		$guildHashes: Object,
@@ -75,6 +76,7 @@ export const IdentifySchema = {
 		$userGuildSettingsVersion: undefined,
 		$guildVersions: Object,
 		$apiCodeVersion: Number,
+		$initialGuildId: String,
 	},
 	$v: Number,
 	$version: Number,
@@ -109,7 +111,11 @@ export interface IdentifySchema {
 	compress?: boolean;
 	large_threshold?: number;
 	largeThreshold?: number;
-	shard?: [bigint, bigint];
+	/**
+	 * @minItems 2
+	 * @maxItems 2
+	 */
+	shard?: bigint[]; // puyo: changed from [bigint, bigint] because it breaks openapi
 	guild_subscriptions?: boolean;
 	capabilities?: number;
 	client_state?: {
@@ -122,6 +128,7 @@ export interface IdentifySchema {
 		private_channels_version?: number;
 		guild_versions?: unknown;
 		api_code_version?: number;
+		initial_guild_id?: string;
 	};
 	clientState?: {
 		guildHashes?: unknown;
@@ -131,6 +138,7 @@ export interface IdentifySchema {
 		useruserGuildSettingsVersion?: number;
 		guildVersions?: unknown;
 		apiCodeVersion?: number;
+		initialGuildId?: string;
 	};
 	v?: number;
 }
diff --git a/src/util/schemas/LazyRequestSchema.ts b/src/util/schemas/LazyRequestSchema.ts
index 63e67416..ee52d66c 100644
--- a/src/util/schemas/LazyRequestSchema.ts
+++ b/src/util/schemas/LazyRequestSchema.ts
@@ -18,7 +18,14 @@
 
 export interface LazyRequestSchema {
 	guild_id: string;
-	channels?: Record<string, [number, number][]>;
+	channels?: {
+		/**
+		 * @items.type integer
+		 * @minItems 2
+		 * @maxItems 2
+		 */
+		[key: string]: number[][]; // puyo: changed from [number, number] because it breaks openapi
+	};
 	activities?: boolean;
 	threads?: boolean;
 	typing?: true;
diff --git a/src/util/schemas/LoginResponse.ts b/src/util/schemas/LoginResponse.ts
new file mode 100644
index 00000000..faf3f769
--- /dev/null
+++ b/src/util/schemas/LoginResponse.ts
@@ -0,0 +1,14 @@
+import { TokenResponse } from "./responses";
+
+export interface MFAResponse {
+	ticket: string;
+	mfa: true;
+	sms: false; // TODO
+	token: null;
+}
+
+export interface WebAuthnResponse extends MFAResponse {
+	webauthn: string;
+}
+
+export type LoginResponse = TokenResponse | MFAResponse | WebAuthnResponse;
diff --git a/src/util/schemas/MemberChangeProfileSchema.ts b/src/util/schemas/MemberChangeProfileSchema.ts
index e955a0f1..d2d1481d 100644
--- a/src/util/schemas/MemberChangeProfileSchema.ts
+++ b/src/util/schemas/MemberChangeProfileSchema.ts
@@ -21,8 +21,7 @@ export interface MemberChangeProfileSchema {
 	nick?: string;
 	bio?: string;
 	pronouns?: string;
-
-	/*
+	/**
 	 * @items.type integer
 	 */
 	theme_colors?: [number, number];
diff --git a/src/util/schemas/UserGuildSettingsSchema.ts b/src/util/schemas/UserGuildSettingsSchema.ts
index c295f767..82edae9c 100644
--- a/src/util/schemas/UserGuildSettingsSchema.ts
+++ b/src/util/schemas/UserGuildSettingsSchema.ts
@@ -16,12 +16,12 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { UserGuildSettings, ChannelOverride } from "@spacebar/util";
+import { ChannelOverride, UserGuildSettings } from "@spacebar/util";
 
 // This sucks. I would use a DeepPartial, my own or typeorms, but they both generate inncorect schema
 export interface UserGuildSettingsSchema
 	extends Partial<Omit<UserGuildSettings, "channel_overrides">> {
 	channel_overrides?: {
-		[channel_id: string]: Partial<ChannelOverride>;
+		[channel_id: string]: ChannelOverride;
 	};
 }
diff --git a/src/util/schemas/UserNoteUpdateSchema.ts b/src/util/schemas/UserNoteUpdateSchema.ts
new file mode 100644
index 00000000..0a731279
--- /dev/null
+++ b/src/util/schemas/UserNoteUpdateSchema.ts
@@ -0,0 +1,3 @@
+export interface UserNoteUpdateSchema {
+	note: string;
+}
diff --git a/src/util/schemas/UserProfileModifySchema.ts b/src/util/schemas/UserProfileModifySchema.ts
index d49fe326..3dea257a 100644
--- a/src/util/schemas/UserProfileModifySchema.ts
+++ b/src/util/schemas/UserProfileModifySchema.ts
@@ -21,8 +21,7 @@ export interface UserProfileModifySchema {
 	accent_color?: number | null;
 	banner?: string | null;
 	pronouns?: string;
-
-	/*
+	/**
 	 * @items.type integer
 	 */
 	theme_colors?: [number, number];
diff --git a/src/util/schemas/UserProfileResponse.ts b/src/util/schemas/UserProfileResponse.ts
deleted file mode 100644
index 699d6a29..00000000
--- a/src/util/schemas/UserProfileResponse.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
-	Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
-	Copyright (C) 2023 Spacebar and Spacebar Contributors
-	
-	This program is free software: you can redistribute it and/or modify
-	it under the terms of the GNU Affero General Public License as published
-	by the Free Software Foundation, either version 3 of the License, or
-	(at your option) any later version.
-	
-	This program is distributed in the hope that it will be useful,
-	but WITHOUT ANY WARRANTY; without even the implied warranty of
-	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-	GNU Affero General Public License for more details.
-	
-	You should have received a copy of the GNU Affero General Public License
-	along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { PublicConnectedAccount, UserPublic } from "..";
-
-export interface UserProfileResponse {
-	user: UserPublic;
-	connected_accounts: PublicConnectedAccount;
-	premium_guild_since?: Date;
-	premium_since?: Date;
-}
diff --git a/src/util/schemas/UserRelationsResponse.ts b/src/util/schemas/UserRelationsResponse.ts
deleted file mode 100644
index 38507420..00000000
--- a/src/util/schemas/UserRelationsResponse.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
-	Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
-	Copyright (C) 2023 Spacebar and Spacebar Contributors
-	
-	This program is free software: you can redistribute it and/or modify
-	it under the terms of the GNU Affero General Public License as published
-	by the Free Software Foundation, either version 3 of the License, or
-	(at your option) any later version.
-	
-	This program is distributed in the hope that it will be useful,
-	but WITHOUT ANY WARRANTY; without even the implied warranty of
-	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-	GNU Affero General Public License for more details.
-	
-	You should have received a copy of the GNU Affero General Public License
-	along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-export interface UserRelationsResponse {
-	object: {
-		id?: string;
-		username?: string;
-		avatar?: string;
-		discriminator?: string;
-		public_flags?: number;
-	};
-}
diff --git a/src/util/schemas/VoiceStateUpdateSchema.ts b/src/util/schemas/VoiceStateUpdateSchema.ts
index a7d9f9d7..fda073e9 100644
--- a/src/util/schemas/VoiceStateUpdateSchema.ts
+++ b/src/util/schemas/VoiceStateUpdateSchema.ts
@@ -26,6 +26,7 @@ export interface VoiceStateUpdateSchema {
 	preferred_region?: string;
 	request_to_speak_timestamp?: Date;
 	suppress?: boolean;
+	flags?: number;
 }
 
 export const VoiceStateUpdateSchema = {
@@ -37,4 +38,5 @@ export const VoiceStateUpdateSchema = {
 	$preferred_region: String,
 	$request_to_speak_timestamp: Date,
 	$suppress: Boolean,
+	$flags: Number,
 };
diff --git a/src/util/schemas/WebAuthnSchema.ts b/src/util/schemas/WebAuthnSchema.ts
index 652cda34..3f5e0da7 100644
--- a/src/util/schemas/WebAuthnSchema.ts
+++ b/src/util/schemas/WebAuthnSchema.ts
@@ -28,9 +28,9 @@ export interface CreateWebAuthnCredentialSchema {
 	ticket: string;
 }
 
-export type WebAuthnPostSchema = Partial<
-	GenerateWebAuthnCredentialsSchema | CreateWebAuthnCredentialSchema
->;
+export type WebAuthnPostSchema =
+	| GenerateWebAuthnCredentialsSchema
+	| CreateWebAuthnCredentialSchema;
 
 export interface WebAuthnTotpSchema {
 	code: string;
diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index 2d254752..44a504cd 100644
--- a/src/util/schemas/index.ts
+++ b/src/util/schemas/index.ts
@@ -69,6 +69,7 @@ export * from "./TotpSchema";
 export * from "./UserDeleteSchema";
 export * from "./UserGuildSettingsSchema";
 export * from "./UserModifySchema";
+export * from "./UserNoteUpdateSchema";
 export * from "./UserProfileModifySchema";
 export * from "./UserSettingsSchema";
 export * from "./Validator";
@@ -79,7 +80,4 @@ export * from "./VoiceVideoSchema";
 export * from "./WebAuthnSchema";
 export * from "./WebhookCreateSchema";
 export * from "./WidgetModifySchema";
-export * from "./UserRelationsResponse";
-export * from "./GatewayResponse";
-export * from "./GatewayBotResponse";
-export * from "./UserProfileResponse";
+export * from "./responses";
diff --git a/src/util/schemas/responses/APIErrorOrCaptchaResponse.ts b/src/util/schemas/responses/APIErrorOrCaptchaResponse.ts
new file mode 100644
index 00000000..c9a0e5be
--- /dev/null
+++ b/src/util/schemas/responses/APIErrorOrCaptchaResponse.ts
@@ -0,0 +1,6 @@
+import { APIErrorResponse } from "./APIErrorResponse";
+import { CaptchaRequiredResponse } from "./CaptchaRequiredResponse";
+
+export type APIErrorOrCaptchaResponse =
+	| CaptchaRequiredResponse
+	| APIErrorResponse;
diff --git a/src/util/schemas/responses/APIErrorResponse.ts b/src/util/schemas/responses/APIErrorResponse.ts
new file mode 100644
index 00000000..25bb9504
--- /dev/null
+++ b/src/util/schemas/responses/APIErrorResponse.ts
@@ -0,0 +1,12 @@
+export interface APIErrorResponse {
+	code: number;
+	message: string;
+	errors: {
+		[key: string]: {
+			_errors: {
+				message: string;
+				code: string;
+			}[];
+		};
+	};
+}
diff --git a/src/util/schemas/responses/BackupCodesChallengeResponse.ts b/src/util/schemas/responses/BackupCodesChallengeResponse.ts
new file mode 100644
index 00000000..5473ad1f
--- /dev/null
+++ b/src/util/schemas/responses/BackupCodesChallengeResponse.ts
@@ -0,0 +1,4 @@
+export interface BackupCodesChallengeResponse {
+	nonce: string;
+	regenerate_nonce: string;
+}
diff --git a/src/util/schemas/responses/CaptchaRequiredResponse.ts b/src/util/schemas/responses/CaptchaRequiredResponse.ts
new file mode 100644
index 00000000..9f7f02ff
--- /dev/null
+++ b/src/util/schemas/responses/CaptchaRequiredResponse.ts
@@ -0,0 +1,5 @@
+export interface CaptchaRequiredResponse {
+	captcha_key: string;
+	captcha_sitekey: string;
+	captcha_service: string;
+}
diff --git a/src/util/schemas/responses/DiscoverableGuildsResponse.ts b/src/util/schemas/responses/DiscoverableGuildsResponse.ts
new file mode 100644
index 00000000..2a9fb1bd
--- /dev/null
+++ b/src/util/schemas/responses/DiscoverableGuildsResponse.ts
@@ -0,0 +1,8 @@
+import { Guild } from "../../entities";
+
+export interface DiscoverableGuildsResponse {
+	total: number;
+	guilds: Guild[];
+	offset: number;
+	limit: number;
+}
diff --git a/src/util/schemas/responses/GatewayBotResponse.ts b/src/util/schemas/responses/GatewayBotResponse.ts
new file mode 100644
index 00000000..30f1f57f
--- /dev/null
+++ b/src/util/schemas/responses/GatewayBotResponse.ts
@@ -0,0 +1,10 @@
+export interface GatewayBotResponse {
+	url: string;
+	shards: number;
+	session_start_limit: {
+		total: number;
+		remaining: number;
+		reset_after: number;
+		max_concurrency: number;
+	};
+}
diff --git a/src/util/schemas/responses/GatewayResponse.ts b/src/util/schemas/responses/GatewayResponse.ts
new file mode 100644
index 00000000..e909f7bd
--- /dev/null
+++ b/src/util/schemas/responses/GatewayResponse.ts
@@ -0,0 +1,3 @@
+export interface GatewayResponse {
+	url: string;
+}
diff --git a/src/util/schemas/responses/GenerateRegistrationTokensResponse.ts b/src/util/schemas/responses/GenerateRegistrationTokensResponse.ts
new file mode 100644
index 00000000..8816eabf
--- /dev/null
+++ b/src/util/schemas/responses/GenerateRegistrationTokensResponse.ts
@@ -0,0 +1,3 @@
+export interface GenerateRegistrationTokensResponse {
+	tokens: string[];
+}
diff --git a/src/util/schemas/responses/GuildBansResponse.ts b/src/util/schemas/responses/GuildBansResponse.ts
new file mode 100644
index 00000000..876a4bc4
--- /dev/null
+++ b/src/util/schemas/responses/GuildBansResponse.ts
@@ -0,0 +1,10 @@
+export interface GuildBansResponse {
+	reason: string;
+	user: {
+		username: string;
+		discriminator: string;
+		id: string;
+		avatar: string | null;
+		public_flags: number;
+	};
+}
diff --git a/src/util/schemas/responses/GuildCreateResponse.ts b/src/util/schemas/responses/GuildCreateResponse.ts
new file mode 100644
index 00000000..8185cb86
--- /dev/null
+++ b/src/util/schemas/responses/GuildCreateResponse.ts
@@ -0,0 +1,3 @@
+export interface GuildCreateResponse {
+	id: string;
+}
diff --git a/src/util/schemas/responses/GuildDiscoveryRequirements.ts b/src/util/schemas/responses/GuildDiscoveryRequirements.ts
new file mode 100644
index 00000000..731976f7
--- /dev/null
+++ b/src/util/schemas/responses/GuildDiscoveryRequirements.ts
@@ -0,0 +1,23 @@
+export interface GuildDiscoveryRequirementsResponse {
+	uild_id: string;
+	safe_environment: boolean;
+	healthy: boolean;
+	health_score_pending: boolean;
+	size: boolean;
+	nsfw_properties: unknown;
+	protected: boolean;
+	sufficient: boolean;
+	sufficient_without_grace_period: boolean;
+	valid_rules_channel: boolean;
+	retention_healthy: boolean;
+	engagement_healthy: boolean;
+	age: boolean;
+	minimum_age: number;
+	health_score: {
+		avg_nonnew_participators: number;
+		avg_nonnew_communicators: number;
+		num_intentful_joiners: number;
+		perc_ret_w1_intentful: number;
+	};
+	minimum_size: number;
+}
diff --git a/src/util/schemas/responses/GuildMessagesSearchResponse.ts b/src/util/schemas/responses/GuildMessagesSearchResponse.ts
new file mode 100644
index 00000000..0b6248b7
--- /dev/null
+++ b/src/util/schemas/responses/GuildMessagesSearchResponse.ts
@@ -0,0 +1,32 @@
+import {
+	Attachment,
+	Embed,
+	MessageType,
+	PublicUser,
+	Role,
+} from "../../entities";
+
+export interface GuildMessagesSearchMessage {
+	id: string;
+	type: MessageType;
+	content?: string;
+	channel_id: string;
+	author: PublicUser;
+	attachments: Attachment[];
+	embeds: Embed[];
+	mentions: PublicUser[];
+	mention_roles: Role[];
+	pinned: boolean;
+	mention_everyone?: boolean;
+	tts: boolean;
+	timestamp: string;
+	edited_timestamp: string | null;
+	flags: number;
+	components: unknown[];
+	hit: true;
+}
+
+export interface GuildMessagesSearchResponse {
+	messages: GuildMessagesSearchMessage[];
+	total_results: number;
+}
diff --git a/src/util/schemas/responses/GuildPruneResponse.ts b/src/util/schemas/responses/GuildPruneResponse.ts
new file mode 100644
index 00000000..fb1abb89
--- /dev/null
+++ b/src/util/schemas/responses/GuildPruneResponse.ts
@@ -0,0 +1,7 @@
+export interface GuildPruneResponse {
+	pruned: number;
+}
+
+export interface GuildPurgeResponse {
+	purged: number;
+}
diff --git a/src/util/schemas/responses/GuildRecommendationsResponse.ts b/src/util/schemas/responses/GuildRecommendationsResponse.ts
new file mode 100644
index 00000000..211670a6
--- /dev/null
+++ b/src/util/schemas/responses/GuildRecommendationsResponse.ts
@@ -0,0 +1,6 @@
+import { Guild } from "../../entities";
+
+export interface GuildRecommendationsResponse {
+	recommended_guilds: Guild[];
+	load_id: string;
+}
diff --git a/src/util/schemas/responses/GuildVanityUrl.ts b/src/util/schemas/responses/GuildVanityUrl.ts
new file mode 100644
index 00000000..ff37bf4e
--- /dev/null
+++ b/src/util/schemas/responses/GuildVanityUrl.ts
@@ -0,0 +1,17 @@
+export interface GuildVanityUrl {
+	code: string;
+	uses: number;
+}
+
+export interface GuildVanityUrlNoInvite {
+	code: null;
+}
+
+export type GuildVanityUrlResponse =
+	| GuildVanityUrl
+	| GuildVanityUrl[]
+	| GuildVanityUrlNoInvite;
+
+export interface GuildVanityUrlCreateResponse {
+	code: string;
+}
diff --git a/src/util/schemas/responses/GuildVoiceRegionsResponse.ts b/src/util/schemas/responses/GuildVoiceRegionsResponse.ts
new file mode 100644
index 00000000..8190d5fd
--- /dev/null
+++ b/src/util/schemas/responses/GuildVoiceRegionsResponse.ts
@@ -0,0 +1,7 @@
+export interface GuildVoiceRegion {
+	id: string;
+	name: string;
+	custom: boolean;
+	deprecated: boolean;
+	optimal: boolean;
+}
diff --git a/src/util/schemas/responses/GuildWidgetJsonResponse.ts b/src/util/schemas/responses/GuildWidgetJsonResponse.ts
new file mode 100644
index 00000000..ef85dd08
--- /dev/null
+++ b/src/util/schemas/responses/GuildWidgetJsonResponse.ts
@@ -0,0 +1,21 @@
+import { ClientStatus } from "../../interfaces";
+
+export interface GuildWidgetJsonResponse {
+	id: string;
+	name: string;
+	instant_invite: string;
+	channels: {
+		id: string;
+		name: string;
+		position: number;
+	}[];
+	members: {
+		id: string;
+		username: string;
+		discriminator: string;
+		avatar: string | null;
+		status: ClientStatus;
+		avatar_url: string;
+	}[];
+	presence_count: number;
+}
diff --git a/src/util/schemas/responses/GuildWidgetSettingsResponse.ts b/src/util/schemas/responses/GuildWidgetSettingsResponse.ts
new file mode 100644
index 00000000..3c6b45ce
--- /dev/null
+++ b/src/util/schemas/responses/GuildWidgetSettingsResponse.ts
@@ -0,0 +1,6 @@
+import { Snowflake } from "../../util";
+
+export interface GuildWidgetSettingsResponse {
+	enabled: boolean;
+	channel_id: Snowflake | null;
+}
diff --git a/src/util/schemas/responses/InstanceDomainsResponse.ts b/src/util/schemas/responses/InstanceDomainsResponse.ts
new file mode 100644
index 00000000..60367492
--- /dev/null
+++ b/src/util/schemas/responses/InstanceDomainsResponse.ts
@@ -0,0 +1,6 @@
+export interface InstanceDomainsResponse {
+	cdn: string;
+	gateway: string;
+	defaultApiVersion: string;
+	apiEndpoint: string;
+}
diff --git a/src/util/schemas/responses/InstancePingResponse.ts b/src/util/schemas/responses/InstancePingResponse.ts
new file mode 100644
index 00000000..5f1a9488
--- /dev/null
+++ b/src/util/schemas/responses/InstancePingResponse.ts
@@ -0,0 +1,13 @@
+export interface InstancePingResponse {
+	ping: "pong!";
+	instance: {
+		id: string;
+		name: string;
+		description: string | null;
+		image: string | null;
+		correspondenceEmail: string | null;
+		correspondenceUserID: string | null;
+		frontPage: string | null;
+		tosPage: string | null;
+	};
+}
diff --git a/src/util/schemas/responses/InstanceStatsResponse.ts b/src/util/schemas/responses/InstanceStatsResponse.ts
new file mode 100644
index 00000000..d24fd434
--- /dev/null
+++ b/src/util/schemas/responses/InstanceStatsResponse.ts
@@ -0,0 +1,8 @@
+export interface InstanceStatsResponse {
+	counts: {
+		user: number;
+		guild: number;
+		message: number;
+		members: number;
+	};
+}
diff --git a/src/util/schemas/responses/LocationMetadataResponse.ts b/src/util/schemas/responses/LocationMetadataResponse.ts
new file mode 100644
index 00000000..55337557
--- /dev/null
+++ b/src/util/schemas/responses/LocationMetadataResponse.ts
@@ -0,0 +1,5 @@
+export interface LocationMetadataResponse {
+	consent_required: boolean;
+	country_code: string;
+	promotional_email_opt_in: { required: true; pre_checked: false };
+}
diff --git a/src/util/schemas/responses/MemberJoinGuildResponse.ts b/src/util/schemas/responses/MemberJoinGuildResponse.ts
new file mode 100644
index 00000000..d7b39d10
--- /dev/null
+++ b/src/util/schemas/responses/MemberJoinGuildResponse.ts
@@ -0,0 +1,8 @@
+import { Emoji, Guild, Role, Sticker } from "../../entities";
+
+export interface MemberJoinGuildResponse {
+	guild: Guild;
+	emojis: Emoji[];
+	roles: Role[];
+	stickers: Sticker[];
+}
diff --git a/src/util/schemas/responses/OAuthAuthorizeResponse.ts b/src/util/schemas/responses/OAuthAuthorizeResponse.ts
new file mode 100644
index 00000000..60d6d2e2
--- /dev/null
+++ b/src/util/schemas/responses/OAuthAuthorizeResponse.ts
@@ -0,0 +1,3 @@
+export interface OAuthAuthorizeResponse {
+	location: string;
+}
diff --git a/src/util/schemas/responses/Tenor.ts b/src/util/schemas/responses/Tenor.ts
new file mode 100644
index 00000000..9dddf9d0
--- /dev/null
+++ b/src/util/schemas/responses/Tenor.ts
@@ -0,0 +1,72 @@
+export enum TenorMediaTypes {
+	gif,
+	mediumgif,
+	tinygif,
+	nanogif,
+	mp4,
+	loopedmp4,
+	tinymp4,
+	nanomp4,
+	webm,
+	tinywebm,
+	nanowebm,
+}
+
+export type TenorMedia = {
+	preview: string;
+	url: string;
+	dims: number[];
+	size: number;
+};
+
+export type TenorGif = {
+	created: number;
+	hasaudio: boolean;
+	id: string;
+	media: { [type in keyof typeof TenorMediaTypes]: TenorMedia }[];
+	tags: string[];
+	title: string;
+	itemurl: string;
+	hascaption: boolean;
+	url: string;
+};
+
+export type TenorCategory = {
+	searchterm: string;
+	path: string;
+	image: string;
+	name: string;
+};
+
+export type TenorCategoriesResults = {
+	tags: TenorCategory[];
+};
+
+export type TenorTrendingResults = {
+	next: string;
+	results: TenorGif[];
+	locale: string;
+};
+
+export type TenorSearchResults = {
+	next: string;
+	results: TenorGif[];
+};
+
+export interface TenorGifResponse {
+	id: string;
+	title: string;
+	url: string;
+	src: string;
+	gif_src: string;
+	width: number;
+	height: number;
+	preview: string;
+}
+
+export interface TenorTrendingResponse {
+	categories: TenorCategoriesResults;
+	gifs: TenorGifResponse[];
+}
+
+export type TenorGifsResponse = TenorGifResponse[];
diff --git a/src/util/schemas/responses/TokenResponse.ts b/src/util/schemas/responses/TokenResponse.ts
new file mode 100644
index 00000000..7e93055a
--- /dev/null
+++ b/src/util/schemas/responses/TokenResponse.ts
@@ -0,0 +1,15 @@
+import { BackupCode, UserSettings } from "../../entities";
+
+export interface TokenResponse {
+	token: string;
+	settings: UserSettings;
+}
+
+export interface TokenOnlyResponse {
+	token: string;
+}
+
+export interface TokenWithBackupCodesResponse {
+	token: string;
+	backup_codes: BackupCode[];
+}
diff --git a/src/util/schemas/responses/TypedResponses.ts b/src/util/schemas/responses/TypedResponses.ts
new file mode 100644
index 00000000..4349b93c
--- /dev/null
+++ b/src/util/schemas/responses/TypedResponses.ts
@@ -0,0 +1,88 @@
+import { GeneralConfiguration, LimitsConfiguration } from "../../config";
+import { DmChannelDTO } from "../../dtos";
+import {
+	Application,
+	BackupCode,
+	Categories,
+	Channel,
+	Emoji,
+	Guild,
+	Invite,
+	Member,
+	Message,
+	PrivateUser,
+	PublicMember,
+	PublicUser,
+	Role,
+	Sticker,
+	StickerPack,
+	Template,
+	Webhook,
+} from "../../entities";
+import { GuildVoiceRegion } from "./GuildVoiceRegionsResponse";
+
+// removes internal properties from the guild class
+export type APIGuild = Omit<
+	Guild,
+	| "afk_channel"
+	| "template"
+	| "owner"
+	| "public_updates_channel"
+	| "rules_channel"
+	| "system_channel"
+	| "widget_channel"
+>;
+
+export type APIPublicUser = PublicUser;
+export type APIPrivateUser = PrivateUser;
+
+export type APIGuildArray = APIGuild[];
+
+export type APIDMChannelArray = DmChannelDTO[];
+
+export type APIBackupCodeArray = BackupCode[];
+
+export interface UserUpdateResponse extends APIPrivateUser {
+	newToken?: string;
+}
+
+export type ApplicationDetectableResponse = unknown[];
+
+export type ApplicationEntitlementsResponse = unknown[];
+
+export type ApplicationSkusResponse = unknown[];
+
+export type APIApplicationArray = Application[];
+
+export type APIInviteArray = Invite[];
+
+export type APIMessageArray = Message[];
+
+export type APIWebhookArray = Webhook[];
+
+export type APIDiscoveryCategoryArray = Categories[];
+
+export type APIGeneralConfiguration = GeneralConfiguration;
+
+export type APIChannelArray = Channel[];
+
+export type APIEmojiArray = Emoji[];
+
+export type APIMemberArray = Member[];
+export type APIPublicMember = PublicMember;
+
+export interface APIGuildWithJoinedAt extends Guild {
+	joined_at: string;
+}
+
+export type APIRoleArray = Role[];
+
+export type APIStickerArray = Sticker[];
+
+export type APITemplateArray = Template[];
+
+export type APIGuildVoiceRegion = GuildVoiceRegion[];
+
+export type APILimitsConfiguration = LimitsConfiguration;
+
+export type APIStickerPackArray = StickerPack[];
diff --git a/src/util/schemas/responses/UpdatesResponse.ts b/src/util/schemas/responses/UpdatesResponse.ts
new file mode 100644
index 00000000..6b8566f4
--- /dev/null
+++ b/src/util/schemas/responses/UpdatesResponse.ts
@@ -0,0 +1,6 @@
+export interface UpdatesResponse {
+	name: string;
+	pub_date: string;
+	url: string;
+	notes: string | null;
+}
diff --git a/src/util/schemas/responses/UserNoteResponse.ts b/src/util/schemas/responses/UserNoteResponse.ts
new file mode 100644
index 00000000..b142811e
--- /dev/null
+++ b/src/util/schemas/responses/UserNoteResponse.ts
@@ -0,0 +1,5 @@
+export interface UserNoteResponse {
+	note: string;
+	note_user_id: string;
+	user_id: string;
+}
diff --git a/src/util/schemas/responses/UserProfileResponse.ts b/src/util/schemas/responses/UserProfileResponse.ts
new file mode 100644
index 00000000..eba7cbcc
--- /dev/null
+++ b/src/util/schemas/responses/UserProfileResponse.ts
@@ -0,0 +1,37 @@
+import {
+	Member,
+	PublicConnectedAccount,
+	PublicMember,
+	PublicUser,
+	User,
+} from "@spacebar/util";
+
+export type MutualGuild = {
+	id: string;
+	nick?: string;
+};
+
+export type PublicMemberProfile = Pick<
+	Member,
+	"banner" | "bio" | "guild_id"
+> & {
+	accent_color: null; // TODO
+};
+
+export type UserProfile = Pick<
+	User,
+	"bio" | "accent_color" | "banner" | "pronouns" | "theme_colors"
+>;
+
+export interface UserProfileResponse {
+	user: PublicUser;
+	connected_accounts: PublicConnectedAccount;
+	premium_guild_since?: Date;
+	premium_since?: Date;
+	mutual_guilds: MutualGuild[];
+	premium_type: number;
+	profile_themes_experiment_bucket: number;
+	user_profile: UserProfile;
+	guild_member?: PublicMember;
+	guild_member_profile?: PublicMemberProfile;
+}
diff --git a/src/util/schemas/responses/UserRelationsResponse.ts b/src/util/schemas/responses/UserRelationsResponse.ts
new file mode 100644
index 00000000..e784cafb
--- /dev/null
+++ b/src/util/schemas/responses/UserRelationsResponse.ts
@@ -0,0 +1,7 @@
+import { User } from "@spacebar/util";
+
+export type UserRelationsResponse = (Pick<User, "id"> &
+	Pick<User, "username"> &
+	Pick<User, "discriminator"> &
+	Pick<User, "avatar"> &
+	Pick<User, "public_flags">)[];
diff --git a/src/util/schemas/responses/UserRelationshipsResponse.ts b/src/util/schemas/responses/UserRelationshipsResponse.ts
new file mode 100644
index 00000000..dff2f118
--- /dev/null
+++ b/src/util/schemas/responses/UserRelationshipsResponse.ts
@@ -0,0 +1,8 @@
+import { PublicUser, RelationshipType } from "../../entities";
+
+export interface UserRelationshipsResponse {
+	id: string;
+	type: RelationshipType;
+	nickname: null;
+	user: PublicUser;
+}
diff --git a/src/util/schemas/responses/WebAuthnCreateResponse.ts b/src/util/schemas/responses/WebAuthnCreateResponse.ts
new file mode 100644
index 00000000..9aa9e206
--- /dev/null
+++ b/src/util/schemas/responses/WebAuthnCreateResponse.ts
@@ -0,0 +1,4 @@
+export interface WebAuthnCreateResponse {
+	name: string;
+	id: string;
+}
diff --git a/src/util/schemas/responses/WebhookCreateResponse.ts b/src/util/schemas/responses/WebhookCreateResponse.ts
new file mode 100644
index 00000000..ae142632
--- /dev/null
+++ b/src/util/schemas/responses/WebhookCreateResponse.ts
@@ -0,0 +1,6 @@
+import { User, Webhook } from "../../entities";
+
+export interface WebhookCreateResponse {
+	user: User;
+	hook: Webhook;
+}
diff --git a/src/util/schemas/responses/index.ts b/src/util/schemas/responses/index.ts
new file mode 100644
index 00000000..d8b7fd57
--- /dev/null
+++ b/src/util/schemas/responses/index.ts
@@ -0,0 +1,34 @@
+export * from "./APIErrorOrCaptchaResponse";
+export * from "./APIErrorResponse";
+export * from "./BackupCodesChallengeResponse";
+export * from "./CaptchaRequiredResponse";
+export * from "./DiscoverableGuildsResponse";
+export * from "./GatewayBotResponse";
+export * from "./GatewayResponse";
+export * from "./GenerateRegistrationTokensResponse";
+export * from "./GuildBansResponse";
+export * from "./GuildCreateResponse";
+export * from "./GuildDiscoveryRequirements";
+export * from "./GuildMessagesSearchResponse";
+export * from "./GuildPruneResponse";
+export * from "./GuildRecommendationsResponse";
+export * from "./GuildVanityUrl";
+export * from "./GuildVoiceRegionsResponse";
+export * from "./GuildWidgetJsonResponse";
+export * from "./GuildWidgetSettingsResponse";
+export * from "./InstanceDomainsResponse";
+export * from "./InstancePingResponse";
+export * from "./InstanceStatsResponse";
+export * from "./LocationMetadataResponse";
+export * from "./MemberJoinGuildResponse";
+export * from "./OAuthAuthorizeResponse";
+export * from "./Tenor";
+export * from "./TokenResponse";
+export * from "./TypedResponses";
+export * from "./UpdatesResponse";
+export * from "./UserNoteResponse";
+export * from "./UserProfileResponse";
+export * from "./UserRelationshipsResponse";
+export * from "./UserRelationsResponse";
+export * from "./WebAuthnCreateResponse";
+export * from "./WebhookCreateResponse";
diff --git a/src/util/util/Application.ts b/src/util/util/Application.ts
new file mode 100644
index 00000000..23019a7f
--- /dev/null
+++ b/src/util/util/Application.ts
@@ -0,0 +1,24 @@
+import { Request } from "express";
+import { Application, User } from "../entities";
+
+export async function createAppBotUser(app: Application, req: Request) {
+	const user = await User.register({
+		username: app.name,
+		password: undefined,
+		id: app.id,
+		req,
+	});
+
+	user.id = app.id;
+	user.premium_since = new Date();
+	user.bot = true;
+
+	await user.save();
+
+	// flags is NaN here?
+	app.assign({ bot: user, flags: app.flags || 0 });
+
+	await app.save();
+
+	return user;
+}
diff --git a/src/util/util/Gifs.ts b/src/util/util/Gifs.ts
new file mode 100644
index 00000000..a5a5e64c
--- /dev/null
+++ b/src/util/util/Gifs.ts
@@ -0,0 +1,25 @@
+import { HTTPError } from "lambert-server";
+import { Config } from "./Config";
+import { TenorGif } from "..";
+
+export function parseGifResult(result: TenorGif) {
+	return {
+		id: result.id,
+		title: result.title,
+		url: result.itemurl,
+		src: result.media[0].mp4.url,
+		gif_src: result.media[0].gif.url,
+		width: result.media[0].mp4.dims[0],
+		height: result.media[0].mp4.dims[1],
+		preview: result.media[0].mp4.preview,
+	};
+}
+
+export function getGifApiKey() {
+	const { enabled, provider, apiKey } = Config.get().gif;
+	if (!enabled) throw new HTTPError(`Gifs are disabled`);
+	if (provider !== "tenor" || !apiKey)
+		throw new HTTPError(`${provider} gif provider not supported`);
+
+	return apiKey;
+}
diff --git a/src/util/util/index.ts b/src/util/util/index.ts
index 838239b7..10e09b5c 100644
--- a/src/util/util/index.ts
+++ b/src/util/util/index.ts
@@ -41,3 +41,5 @@ export * from "./String";
 export * from "./Token";
 export * from "./TraverseDirectory";
 export * from "./WebAuthn";
+export * from "./Gifs";
+export * from "./Application";
diff --git a/src/webrtc/opcodes/SelectProtocol.ts b/src/webrtc/opcodes/SelectProtocol.ts
index 6618d83b..0a06e722 100644
--- a/src/webrtc/opcodes/SelectProtocol.ts
+++ b/src/webrtc/opcodes/SelectProtocol.ts
@@ -18,7 +18,7 @@
 
 import { Payload, Send, WebSocket } from "@spacebar/gateway";
 import { SelectProtocolSchema, validateSchema } from "@spacebar/util";
-import { endpoint, PublicIP, VoiceOPCodes } from "@spacebar/webrtc";
+import { PublicIP, VoiceOPCodes, endpoint } from "@spacebar/webrtc";
 import SemanticSDP, { MediaInfo, SDPInfo } from "semantic-sdp";
 
 export async function onSelectProtocol(this: WebSocket, payload: Payload) {
@@ -56,7 +56,7 @@ export async function onSelectProtocol(this: WebSocket, payload: Payload) {
 		`a=candidate:1 1 ${candidate.getTransport()} ${candidate.getFoundation()} ${candidate.getAddress()} ${candidate.getPort()} typ host`;
 
 	await Send(this, {
-		op: VoiceOPCodes.SELECT_PROTOCOL_ACK,
+		op: VoiceOPCodes.SESSION_DESCRIPTION,
 		d: {
 			video_codec: "H264",
 			sdp: answer,
diff --git a/src/webrtc/util/Constants.ts b/src/webrtc/util/Constants.ts
index bd89c974..dba1c511 100644
--- a/src/webrtc/util/Constants.ts
+++ b/src/webrtc/util/Constants.ts
@@ -29,7 +29,7 @@ export enum VoiceOPCodes {
 	SELECT_PROTOCOL = 1,
 	READY = 2,
 	HEARTBEAT = 3,
-	SELECT_PROTOCOL_ACK = 4,
+	SESSION_DESCRIPTION = 4,
 	SPEAKING = 5,
 	HEARTBEAT_ACK = 6,
 	RESUME = 7,