summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/middlewares/Authentication.ts9
-rw-r--r--src/api/middlewares/RateLimit.ts26
-rw-r--r--src/api/middlewares/Translation.ts2
-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.ts18
-rw-r--r--src/api/routes/auth/verify/index.ts16
-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.ts235
-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/#connection_id/refresh.ts2
-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.ts68
-rw-r--r--src/api/routes/gifs/trending-gifs.ts63
-rw-r--r--src/api/routes/gifs/trending.ts180
-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.ts159
-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.ts71
-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/access-token.ts2
-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/connections/BattleNet/index.ts35
-rw-r--r--src/connections/Discord/index.ts22
-rw-r--r--src/connections/EpicGames/index.ts23
-rw-r--r--src/connections/Facebook/index.ts25
-rw-r--r--src/connections/GitHub/index.ts25
-rw-r--r--src/connections/Reddit/index.ts18
-rw-r--r--src/connections/Spotify/index.ts26
-rw-r--r--src/connections/Twitch/index.ts28
-rw-r--r--src/connections/Twitter/index.ts38
-rw-r--r--src/connections/Xbox/index.ts32
-rw-r--r--src/connections/Youtube/index.ts22
-rw-r--r--src/gateway/events/Close.ts10
-rw-r--r--src/gateway/events/Connection.ts2
-rw-r--r--src/gateway/opcodes/Heartbeat.ts2
-rw-r--r--src/gateway/opcodes/Identify.ts567
-rw-r--r--src/gateway/opcodes/LazyRequest.ts33
-rw-r--r--src/gateway/util/Capabilities.ts26
-rw-r--r--src/gateway/util/WebSocket.ts2
-rw-r--r--src/gateway/util/index.ts1
-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/connections/Connection.ts2
-rw-r--r--src/util/connections/ConnectionLoader.ts8
-rw-r--r--src/util/connections/ConnectionStore.ts4
-rw-r--r--src/util/connections/RefreshableConnection.ts5
-rw-r--r--src/util/dtos/ConnectedAccountDTO.ts2
-rw-r--r--src/util/dtos/ReadyGuildDTO.ts70
-rw-r--r--src/util/entities/Channel.ts68
-rw-r--r--src/util/entities/ConnectedAccount.ts1
-rw-r--r--src/util/entities/Guild.ts52
-rw-r--r--src/util/entities/Member.ts10
-rw-r--r--src/util/entities/Message.ts25
-rw-r--r--src/util/entities/Role.ts3
-rw-r--r--src/util/entities/User.ts35
-rw-r--r--src/util/interfaces/Activity.ts2
-rw-r--r--src/util/interfaces/Event.ts37
-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/ConnectedAccountSchema.ts2
-rw-r--r--src/util/schemas/ConnectionCallbackSchema.ts2
-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/MessageCreateSchema.ts2
-rw-r--r--src/util/schemas/RegisterSchema.ts4
-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/AutoUpdate.ts2
-rw-r--r--src/util/util/Gifs.ts25
-rw-r--r--src/util/util/JSON.ts10
-rw-r--r--src/util/util/Token.ts133
-rw-r--r--src/util/util/index.ts2
-rw-r--r--src/webrtc/opcodes/SelectProtocol.ts4
-rw-r--r--src/webrtc/util/Constants.ts2
210 files changed, 5586 insertions, 2652 deletions
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index d0e4d8a0..9e41b453 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -16,7 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { checkToken, Config, Rights } from "@spacebar/util";
+import { checkToken, Rights } from "@spacebar/util";
 import * as Sentry from "@sentry/node";
 import { NextFunction, Request, Response } from "express";
 import { HTTPError } from "lambert-server";
@@ -92,12 +92,7 @@ export async function Authentication(
 	Sentry.setUser({ id: req.user_id });
 
 	try {
-		const { jwtSecret } = Config.get().security;
-
-		const { decoded, user } = await checkToken(
-			req.headers.authorization,
-			jwtSecret,
-		);
+		const { decoded, user } = await checkToken(req.headers.authorization);
 
 		req.token = decoded;
 		req.user_id = decoded.id;
diff --git a/src/api/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts
index 0da292e9..f5bfbb4f 100644
--- a/src/api/middlewares/RateLimit.ts
+++ b/src/api/middlewares/RateLimit.ts
@@ -83,6 +83,13 @@ export default function rateLimit(opts: {
 
 		const offender = Cache.get(executor_id + bucket_id);
 
+		res.set("X-RateLimit-Limit", `${max_hits}`)
+			.set("X-RateLimit-Remaining", `${max_hits - (offender?.hits || 0)}`)
+			.set("X-RateLimit-Bucket", `${bucket_id}`)
+			// assuming we aren't blocked, a new window will start after this request
+			.set("X-RateLimit-Reset", `${Date.now() + opts.window}`)
+			.set("X-RateLimit-Reset-After", `${opts.window}`);
+
 		if (offender) {
 			let reset = offender.expires_at.getTime();
 			let resetAfterMs = reset - Date.now();
@@ -96,6 +103,12 @@ export default function rateLimit(opts: {
 				Cache.delete(executor_id + bucket_id);
 			}
 
+			res.set("X-RateLimit-Reset", `${reset}`);
+			res.set(
+				"X-RateLimit-Reset-After",
+				`${Math.max(0, Math.ceil(resetAfterSec))}`,
+			);
+
 			if (offender.blocked) {
 				const global = bucket_id === "global";
 				// each block violation pushes the expiry one full window further
@@ -109,16 +122,17 @@ export default function rateLimit(opts: {
 				console.log(`blocked bucket: ${bucket_id} ${executor_id}`, {
 					resetAfterMs,
 				});
+
+				if (global) res.set("X-RateLimit-Global", "true");
+
 				return (
 					res
 						.status(429)
-						.set("X-RateLimit-Limit", `${max_hits}`)
 						.set("X-RateLimit-Remaining", "0")
-						.set("X-RateLimit-Reset", `${reset}`)
-						.set("X-RateLimit-Reset-After", `${resetAfterSec}`)
-						.set("X-RateLimit-Global", `${global}`)
-						.set("Retry-After", `${Math.ceil(resetAfterSec)}`)
-						.set("X-RateLimit-Bucket", `${bucket_id}`)
+						.set(
+							"Retry-After",
+							`${Math.max(0, Math.ceil(resetAfterSec))}`,
+						)
 						// TODO: error rate limit message translation
 						.send({
 							message: "You are being rate limited.",
diff --git a/src/api/middlewares/Translation.ts b/src/api/middlewares/Translation.ts
index 60ff4ad7..f3a4c8df 100644
--- a/src/api/middlewares/Translation.ts
+++ b/src/api/middlewares/Translation.ts
@@ -20,7 +20,7 @@ import fs from "fs";
 import path from "path";
 import i18next from "i18next";
 import i18nextMiddleware from "i18next-http-middleware";
-import i18nextBackend from "i18next-node-fs-backend";
+import i18nextBackend from "i18next-fs-backend";
 import { Router } from "express";
 
 const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
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..b3ca1e9e 100644
--- a/src/api/routes/auth/reset.ts
+++ b/src/api/routes/auth/reset.ts
@@ -19,7 +19,6 @@
 import { route } from "@spacebar/api";
 import {
 	checkToken,
-	Config,
 	Email,
 	FieldErrors,
 	generateToken,
@@ -31,17 +30,26 @@ 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;
 
-		const { jwtSecret } = Config.get().security;
-
 		let user;
 		try {
-			const userTokenData = await checkToken(token, jwtSecret, true);
+			const userTokenData = await checkToken(token);
 			user = userTokenData.user;
 		} catch {
 			throw FieldErrors({
diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts
index c1afcde9..49f74277 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;
 
@@ -67,11 +78,10 @@ router.post(
 			}
 		}
 
-		const { jwtSecret } = Config.get().security;
 		let user;
 
 		try {
-			const userTokenData = await checkToken(token, jwtSecret, true);
+			const userTokenData = await checkToken(token);
 			user = userTokenData.user;
 		} catch {
 			throw FieldErrors({
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..c384a05b 100644
--- a/src/api/routes/channels/#channel_id/messages/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -16,128 +16,137 @@
 	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
-	}
-
-	const messages = await Message.find(query);
-	const endpoint = Config.get().cdn.endpointPublic;
-
-	return res.json(
-		messages.map((x: Partial<Message>) => {
+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 (around) {
+			query.take = Math.floor(limit / 2);
+			const [right, left] = await Promise.all([
+				Message.find({ ...query, where: { id: LessThan(around) } }),
+				Message.find({ ...query, where: { id: MoreThan(around) } }),
+			]);
+			right.push(...left);
+			messages = right;
+		} else {
+			if (after) {
+				if (BigInt(after) > BigInt(Snowflake.generate()))
+					return res.status(422);
+				query.where.id = MoreThan(after);
+			} else if (before) {
+				if (BigInt(before) > BigInt(Snowflake.generate()))
+					return res.status(422);
+				query.where.id = LessThan(before);
+			}
+
+			messages = await Message.find(query);
+		}
+
+		const endpoint = Config.get().cdn.endpointPublic;
+
+		const ret = messages.map((x: Message) => {
+			x = x.toJSON();
+
 			(x.reactions || []).forEach((y: Partial<Reaction>) => {
 				// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 				//@ts-ignore
@@ -172,9 +181,11 @@ router.get("/", route({}), async (req: Request, res: Response) => {
 			// }
 
 			return x;
-		}),
-	);
-});
+		});
+
+		return res.json(ret);
+	},
+);
 
 // TODO: config max upload size
 const messageUpload = multer({
@@ -205,9 +216,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;
@@ -288,9 +309,11 @@ router.post(
 			embeds,
 			channel_id,
 			attachments,
-			edited_timestamp: undefined,
 			timestamp: new Date(),
 		});
+		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+		//@ts-ignore dont care2
+		message.edited_timestamp = null;
 
 		channel.last_message_id = message.id;
 
@@ -366,3 +389,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/#connection_id/refresh.ts b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts
index 0d432c2b..d44cf314 100644
--- a/src/api/routes/connections/#connection_name/#connection_id/refresh.ts
+++ b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts
@@ -22,7 +22,7 @@ const router = Router();
 
 router.post("/", route({}), async (req: Request, res: Response) => {
 	// TODO:
-	const { connection_name, connection_id } = req.params;
+	// const { connection_name, connection_id } = req.params;
 	res.sendStatus(204);
 });
 
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..305a2a48 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 fetch from "node-fetch";
-import ProxyAgent from "proxy-agent";
 import { route } from "@spacebar/api";
-import { getGifApiKey, parseGifResult } from "./trending";
+import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import fetch from "node-fetch";
+import { ProxyAgent } from "proxy-agent";
 
 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..77a61efc 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 fetch from "node-fetch";
-import ProxyAgent from "proxy-agent";
 import { route } from "@spacebar/api";
-import { getGifApiKey, parseGifResult } from "./trending";
+import { TenorMediaTypes, getGifApiKey, parseGifResult } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import fetch from "node-fetch";
+import { ProxyAgent } from "proxy-agent";
 
 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..fe726842 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 fetch from "node-fetch";
-import ProxyAgent from "proxy-agent";
 import { route } from "@spacebar/api";
-import { Config } from "@spacebar/util";
-import { HTTPError } from "lambert-server";
+import {
+	TenorCategoriesResults,
+	TenorTrendingResults,
+	getGifApiKey,
+	parseGifResult,
+} from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import fetch from "node-fetch";
+import { ProxyAgent } from "proxy-agent";
 
 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..6feb0a83 100644
--- a/src/api/routes/guilds/#guild_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/index.ts
@@ -16,46 +16,81 @@
 	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,
 	DiscordApiErrors,
-	emitEvent,
-	getPermission,
-	getRights,
 	Guild,
 	GuildUpdateEvent,
-	handleFile,
-	Member,
 	GuildUpdateSchema,
+	Member,
+	Permissions,
 	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;
@@ -122,13 +157,83 @@ router.patch(
 			guild.features = body.features;
 		}
 
+		if (body.public_updates_channel_id == "1") {
+			// move all channels up 1
+			await Channel.createQueryBuilder("channels")
+				.where({ guild: { id: guild_id } })
+				.update({ position: () => "position + 1" })
+				.execute();
+
+			// create an updates channel for them
+			await Channel.createChannel(
+				{
+					name: "moderator-only",
+					guild_id: guild.id,
+					position: 0,
+					type: 0,
+					permission_overwrites: [
+						// remove SEND_MESSAGES from @everyone
+						{
+							id: guild.id,
+							allow: "0",
+							deny: Permissions.FLAGS.VIEW_CHANNEL.toString(),
+							type: 0,
+						},
+					],
+				},
+				undefined,
+				{ skipPermissionCheck: true },
+			);
+		} else if (body.public_updates_channel_id != undefined) {
+			// ensure channel exists in this guild
+			await Channel.findOneOrFail({
+				where: { guild_id, id: body.public_updates_channel_id },
+				select: { id: true },
+			});
+		}
+
+		if (body.rules_channel_id == "1") {
+			// move all channels up 1
+			await Channel.createQueryBuilder("channels")
+				.where({ guild: { id: guild_id } })
+				.update({ position: () => "position + 1" })
+				.execute();
+
+			// create a rules for them
+			await Channel.createChannel(
+				{
+					name: "rules",
+					guild_id: guild.id,
+					position: 0,
+					type: 0,
+					permission_overwrites: [
+						// remove SEND_MESSAGES from @everyone
+						{
+							id: guild.id,
+							allow: "0",
+							deny: Permissions.FLAGS.SEND_MESSAGES.toString(),
+							type: 0,
+						},
+					],
+				},
+				undefined,
+				{ skipPermissionCheck: true },
+			);
+		} else if (body.rules_channel_id != undefined) {
+			// ensure channel exists in this guild
+			await Channel.findOneOrFail({
+				where: { guild_id, id: body.rules_channel_id },
+				select: { id: true },
+			});
+		}
+
 		// TODO: check if body ids are valid
 		guild.assign(body);
 
 		const data = guild.toJSON();
 		// TODO: guild hashes
 		// TODO: fix vanity_url_code, template_id
-		delete data.vanity_url_code;
+		// delete data.vanity_url_code;
 		delete data.template_id;
 
 		await Promise.all([
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..81000b4b 100644
--- a/src/api/routes/guilds/#guild_id/welcome-screen.ts
+++ b/src/api/routes/guilds/#guild_id/welcome-screen.ts
@@ -16,27 +16,53 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
-import { Guild, Member, GuildUpdateWelcomeScreenSchema } from "@spacebar/util";
-import { HTTPError } from "lambert-server";
 import { route } from "@spacebar/api";
+import {
+	Channel,
+	Guild,
+	GuildUpdateWelcomeScreenSchema,
+	Member,
+} from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 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;
@@ -44,17 +70,28 @@ router.patch(
 
 		const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
 
-		if (!guild.welcome_screen.enabled)
-			throw new HTTPError("Welcome screen disabled", 400);
-		if (body.welcome_channels)
-			guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid
-		if (body.description)
+		if (body.enabled != undefined)
+			guild.welcome_screen.enabled = body.enabled;
+
+		if (body.description != undefined)
 			guild.welcome_screen.description = body.description;
-		if (body.enabled != null) guild.welcome_screen.enabled = body.enabled;
+
+		if (body.welcome_channels != undefined) {
+			// Ensure channels exist within the guild
+			await Promise.all(
+				body.welcome_channels?.map(({ channel_id }) =>
+					Channel.findOneOrFail({
+						where: { id: channel_id, guild_id },
+						select: { id: true },
+					}),
+				) || [],
+			);
+			guild.welcome_screen.welcome_channels = body.welcome_channels;
+		}
 
 		await guild.save();
 
-		res.sendStatus(204);
+		res.status(200).json(guild.welcome_screen);
 	},
 );
 
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 8bff2200..28a3b429 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/access-token.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts
index 9031f3c8..789a7878 100644
--- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts
+++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts
@@ -23,9 +23,9 @@ import {
 	ConnectionStore,
 	DiscordApiErrors,
 	FieldErrors,
+	RefreshableConnection,
 } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import RefreshableConnection from "../../../../../../../util/connections/RefreshableConnection";
 const router = Router();
 
 // TODO: this route is only used for spotify, twitch, and youtube. (battlenet seems to be able to PUT, maybe others also)
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/connections/BattleNet/index.ts b/src/connections/BattleNet/index.ts
index 7edc2e92..4fdfccb1 100644
--- a/src/connections/BattleNet/index.ts
+++ b/src/connections/BattleNet/index.ts
@@ -19,12 +19,12 @@
 import {
 	ConnectedAccount,
 	ConnectedAccountCommonOAuthTokenResponse,
+	Connection,
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
 } from "@spacebar/util";
 import wretch from "wretch";
-import Connection from "../../util/connections/Connection";
 import { BattleNetSettings } from "./BattleNetSettings";
 
 interface BattleNetConnectionUser {
@@ -33,10 +33,10 @@ interface BattleNetConnectionUser {
 	battletag: string;
 }
 
-interface BattleNetErrorResponse {
-	error: string;
-	error_description: string;
-}
+// interface BattleNetErrorResponse {
+// 	error: string;
+// 	error_description: string;
+// }
 
 export default class BattleNetConnection extends Connection {
 	public readonly id = "battlenet";
@@ -47,17 +47,21 @@ export default class BattleNetConnection extends Connection {
 	settings: BattleNetSettings = new BattleNetSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
-			this.id,
-			this.settings,
-		) as BattleNetSettings;
+		const settings =
+			ConnectionLoader.getConnectionConfig<BattleNetSettings>(
+				this.id,
+				this.settings,
+			);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("scope", this.scopes.join(" "));
 		url.searchParams.append("state", state);
@@ -85,8 +89,8 @@ export default class BattleNetConnection extends Connection {
 				new URLSearchParams({
 					grant_type: "authorization_code",
 					code: code,
-					client_id: this.settings.clientId!,
-					client_secret: this.settings.clientSecret!,
+					client_id: this.settings.clientId as string,
+					client_secret: this.settings.clientSecret as string,
 					redirect_uri: this.getRedirectUri(),
 				}),
 			)
@@ -115,8 +119,11 @@ export default class BattleNetConnection extends Connection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo.id.toString());
diff --git a/src/connections/Discord/index.ts b/src/connections/Discord/index.ts
index 76de33be..731086f1 100644
--- a/src/connections/Discord/index.ts
+++ b/src/connections/Discord/index.ts
@@ -19,12 +19,12 @@
 import {
 	ConnectedAccount,
 	ConnectedAccountCommonOAuthTokenResponse,
+	Connection,
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
 } from "@spacebar/util";
 import wretch from "wretch";
-import Connection from "../../util/connections/Connection";
 import { DiscordSettings } from "./DiscordSettings";
 
 interface UserResponse {
@@ -43,10 +43,13 @@ export default class DiscordConnection extends Connection {
 	settings: DiscordSettings = new DiscordSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
+		const settings = ConnectionLoader.getConnectionConfig<DiscordSettings>(
 			this.id,
 			this.settings,
-		) as DiscordSettings;
+		);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
@@ -54,7 +57,7 @@ export default class DiscordConnection extends Connection {
 		const url = new URL(this.authorizeUrl);
 
 		url.searchParams.append("state", state);
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("scope", this.scopes.join(" "));
 		url.searchParams.append("response_type", "code");
 		// controls whether, on repeated authorizations, the consent screen is shown
@@ -82,8 +85,8 @@ export default class DiscordConnection extends Connection {
 			})
 			.body(
 				new URLSearchParams({
-					client_id: this.settings.clientId!,
-					client_secret: this.settings.clientSecret!,
+					client_id: this.settings.clientId as string,
+					client_secret: this.settings.clientSecret as string,
 					grant_type: "authorization_code",
 					code: code,
 					redirect_uri: this.getRedirectUri(),
@@ -114,8 +117,11 @@ export default class DiscordConnection extends Connection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo.id);
diff --git a/src/connections/EpicGames/index.ts b/src/connections/EpicGames/index.ts
index bd7c7eef..e5b2d336 100644
--- a/src/connections/EpicGames/index.ts
+++ b/src/connections/EpicGames/index.ts
@@ -19,12 +19,12 @@
 import {
 	ConnectedAccount,
 	ConnectedAccountCommonOAuthTokenResponse,
+	Connection,
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
 } from "@spacebar/util";
 import wretch from "wretch";
-import Connection from "../../util/connections/Connection";
 import { EpicGamesSettings } from "./EpicGamesSettings";
 
 export interface UserResponse {
@@ -53,17 +53,21 @@ export default class EpicGamesConnection extends Connection {
 	settings: EpicGamesSettings = new EpicGamesSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
-			this.id,
-			this.settings,
-		) as EpicGamesSettings;
+		const settings =
+			ConnectionLoader.getConnectionConfig<EpicGamesSettings>(
+				this.id,
+				this.settings,
+			);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("response_type", "code");
 		url.searchParams.append("scope", this.scopes.join(" "));
@@ -127,8 +131,11 @@ export default class EpicGamesConnection extends Connection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo[0].accountId);
diff --git a/src/connections/Facebook/index.ts b/src/connections/Facebook/index.ts
index 6ce722dd..2bf26f34 100644
--- a/src/connections/Facebook/index.ts
+++ b/src/connections/Facebook/index.ts
@@ -19,12 +19,12 @@
 import {
 	ConnectedAccount,
 	ConnectedAccountCommonOAuthTokenResponse,
+	Connection,
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
 } from "@spacebar/util";
 import wretch from "wretch";
-import Connection from "../../util/connections/Connection";
 import { FacebookSettings } from "./FacebookSettings";
 
 export interface FacebookErrorResponse {
@@ -52,17 +52,20 @@ export default class FacebookConnection extends Connection {
 	settings: FacebookSettings = new FacebookSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
+		const settings = ConnectionLoader.getConnectionConfig<FacebookSettings>(
 			this.id,
 			this.settings,
-		) as FacebookSettings;
+		);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("state", state);
 		url.searchParams.append("response_type", "code");
@@ -73,8 +76,11 @@ export default class FacebookConnection extends Connection {
 
 	getTokenUrl(code: string): string {
 		const url = new URL(this.tokenUrl);
-		url.searchParams.append("client_id", this.settings.clientId!);
-		url.searchParams.append("client_secret", this.settings.clientSecret!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
+		url.searchParams.append(
+			"client_secret",
+			this.settings.clientSecret as string,
+		);
 		url.searchParams.append("code", code);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		return url.toString();
@@ -118,8 +124,11 @@ export default class FacebookConnection extends Connection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo.id);
diff --git a/src/connections/GitHub/index.ts b/src/connections/GitHub/index.ts
index a675873f..25e5f89f 100644
--- a/src/connections/GitHub/index.ts
+++ b/src/connections/GitHub/index.ts
@@ -19,12 +19,12 @@
 import {
 	ConnectedAccount,
 	ConnectedAccountCommonOAuthTokenResponse,
+	Connection,
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
 } from "@spacebar/util";
 import wretch from "wretch";
-import Connection from "../../util/connections/Connection";
 import { GitHubSettings } from "./GitHubSettings";
 
 interface UserResponse {
@@ -42,17 +42,20 @@ export default class GitHubConnection extends Connection {
 	settings: GitHubSettings = new GitHubSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
+		const settings = ConnectionLoader.getConnectionConfig<GitHubSettings>(
 			this.id,
 			this.settings,
-		) as GitHubSettings;
+		);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("scope", this.scopes.join(" "));
 		url.searchParams.append("state", state);
@@ -61,8 +64,11 @@ export default class GitHubConnection extends Connection {
 
 	getTokenUrl(code: string): string {
 		const url = new URL(this.tokenUrl);
-		url.searchParams.append("client_id", this.settings.clientId!);
-		url.searchParams.append("client_secret", this.settings.clientSecret!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
+		url.searchParams.append(
+			"client_secret",
+			this.settings.clientSecret as string,
+		);
 		url.searchParams.append("code", code);
 		return url.toString();
 	}
@@ -105,8 +111,11 @@ export default class GitHubConnection extends Connection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo.id.toString());
diff --git a/src/connections/Reddit/index.ts b/src/connections/Reddit/index.ts
index 191c6452..149cce02 100644
--- a/src/connections/Reddit/index.ts
+++ b/src/connections/Reddit/index.ts
@@ -19,12 +19,12 @@
 import {
 	ConnectedAccount,
 	ConnectedAccountCommonOAuthTokenResponse,
+	Connection,
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
 } from "@spacebar/util";
 import wretch from "wretch";
-import Connection from "../../util/connections/Connection";
 import { RedditSettings } from "./RedditSettings";
 
 export interface UserResponse {
@@ -54,17 +54,20 @@ export default class RedditConnection extends Connection {
 	settings: RedditSettings = new RedditSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
+		const settings = ConnectionLoader.getConnectionConfig<RedditSettings>(
 			this.id,
 			this.settings,
-		) as RedditSettings;
+		);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("response_type", "code");
 		url.searchParams.append("scope", this.scopes.join(" "));
@@ -124,8 +127,11 @@ export default class RedditConnection extends Connection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo.id.toString());
diff --git a/src/connections/Spotify/index.ts b/src/connections/Spotify/index.ts
index 61b17366..ece404d8 100644
--- a/src/connections/Spotify/index.ts
+++ b/src/connections/Spotify/index.ts
@@ -22,9 +22,9 @@ import {
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
+	RefreshableConnection,
 } from "@spacebar/util";
 import wretch from "wretch";
-import RefreshableConnection from "../../util/connections/RefreshableConnection";
 import { SpotifySettings } from "./SpotifySettings";
 
 export interface UserResponse {
@@ -63,17 +63,20 @@ export default class SpotifyConnection extends RefreshableConnection {
 		 * So to prevent spamming the spotify api we disable the ability to refresh.
 		 */
 		this.refreshEnabled = false;
-		this.settings = ConnectionLoader.getConnectionConfig(
+		const settings = ConnectionLoader.getConnectionConfig<SpotifySettings>(
 			this.id,
 			this.settings,
-		) as SpotifySettings;
+		);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("response_type", "code");
 		url.searchParams.append("scope", this.scopes.join(" "));
@@ -98,7 +101,9 @@ export default class SpotifyConnection extends RefreshableConnection {
 				Accept: "application/json",
 				"Content-Type": "application/x-www-form-urlencoded",
 				Authorization: `Basic ${Buffer.from(
-					`${this.settings.clientId!}:${this.settings.clientSecret!}`,
+					`${this.settings.clientId as string}:${
+						this.settings.clientSecret as string
+					}`,
 				).toString("base64")}`,
 			})
 			.body(
@@ -129,7 +134,9 @@ export default class SpotifyConnection extends RefreshableConnection {
 				Accept: "application/json",
 				"Content-Type": "application/x-www-form-urlencoded",
 				Authorization: `Basic ${Buffer.from(
-					`${this.settings.clientId!}:${this.settings.clientSecret!}`,
+					`${this.settings.clientId as string}:${
+						this.settings.clientSecret as string
+					}`,
 				).toString("base64")}`,
 			})
 			.body(
@@ -169,8 +176,11 @@ export default class SpotifyConnection extends RefreshableConnection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo.id);
diff --git a/src/connections/Twitch/index.ts b/src/connections/Twitch/index.ts
index 6d679aa4..9a6cea35 100644
--- a/src/connections/Twitch/index.ts
+++ b/src/connections/Twitch/index.ts
@@ -22,9 +22,9 @@ import {
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
+	RefreshableConnection,
 } from "@spacebar/util";
 import wretch from "wretch";
-import RefreshableConnection from "../../util/connections/RefreshableConnection";
 import { TwitchSettings } from "./TwitchSettings";
 
 interface TwitchConnectionUserResponse {
@@ -55,17 +55,20 @@ export default class TwitchConnection extends RefreshableConnection {
 	settings: TwitchSettings = new TwitchSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
+		const settings = ConnectionLoader.getConnectionConfig<TwitchSettings>(
 			this.id,
 			this.settings,
-		) as TwitchSettings;
+		);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("response_type", "code");
 		url.searchParams.append("scope", this.scopes.join(" "));
@@ -94,8 +97,8 @@ export default class TwitchConnection extends RefreshableConnection {
 				new URLSearchParams({
 					grant_type: "authorization_code",
 					code: code,
-					client_id: this.settings.clientId!,
-					client_secret: this.settings.clientSecret!,
+					client_id: this.settings.clientId as string,
+					client_secret: this.settings.clientSecret as string,
 					redirect_uri: this.getRedirectUri(),
 				}),
 			)
@@ -124,8 +127,8 @@ export default class TwitchConnection extends RefreshableConnection {
 			.body(
 				new URLSearchParams({
 					grant_type: "refresh_token",
-					client_id: this.settings.clientId!,
-					client_secret: this.settings.clientSecret!,
+					client_id: this.settings.clientId as string,
+					client_secret: this.settings.clientSecret as string,
 					refresh_token: refresh_token,
 				}),
 			)
@@ -148,7 +151,7 @@ export default class TwitchConnection extends RefreshableConnection {
 		return wretch(url.toString())
 			.headers({
 				Authorization: `Bearer ${token}`,
-				"Client-Id": this.settings.clientId!,
+				"Client-Id": this.settings.clientId as string,
 			})
 			.get()
 			.json<TwitchConnectionUserResponse>()
@@ -161,8 +164,11 @@ export default class TwitchConnection extends RefreshableConnection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo.data[0].id);
diff --git a/src/connections/Twitter/index.ts b/src/connections/Twitter/index.ts
index aa48ca12..62fd7da1 100644
--- a/src/connections/Twitter/index.ts
+++ b/src/connections/Twitter/index.ts
@@ -22,9 +22,9 @@ import {
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
+	RefreshableConnection,
 } from "@spacebar/util";
 import wretch from "wretch";
-import RefreshableConnection from "../../util/connections/RefreshableConnection";
 import { TwitterSettings } from "./TwitterSettings";
 
 interface TwitterUserResponse {
@@ -40,10 +40,10 @@ interface TwitterUserResponse {
 	};
 }
 
-interface TwitterErrorResponse {
-	error: string;
-	error_description: string;
-}
+// interface TwitterErrorResponse {
+// 	error: string;
+// 	error_description: string;
+// }
 
 export default class TwitterConnection extends RefreshableConnection {
 	public readonly id = "twitter";
@@ -55,17 +55,20 @@ export default class TwitterConnection extends RefreshableConnection {
 	settings: TwitterSettings = new TwitterSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
+		const settings = ConnectionLoader.getConnectionConfig<TwitterSettings>(
 			this.id,
 			this.settings,
-		) as TwitterSettings;
+		);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("response_type", "code");
 		url.searchParams.append("scope", this.scopes.join(" "));
@@ -92,14 +95,16 @@ export default class TwitterConnection extends RefreshableConnection {
 				Accept: "application/json",
 				"Content-Type": "application/x-www-form-urlencoded",
 				Authorization: `Basic ${Buffer.from(
-					`${this.settings.clientId!}:${this.settings.clientSecret!}`,
+					`${this.settings.clientId as string}:${
+						this.settings.clientSecret as string
+					}`,
 				).toString("base64")}`,
 			})
 			.body(
 				new URLSearchParams({
 					grant_type: "authorization_code",
 					code: code,
-					client_id: this.settings.clientId!,
+					client_id: this.settings.clientId as string,
 					redirect_uri: this.getRedirectUri(),
 					code_verifier: "challenge", // TODO: properly use PKCE challenge
 				}),
@@ -126,14 +131,16 @@ export default class TwitterConnection extends RefreshableConnection {
 				Accept: "application/json",
 				"Content-Type": "application/x-www-form-urlencoded",
 				Authorization: `Basic ${Buffer.from(
-					`${this.settings.clientId!}:${this.settings.clientSecret!}`,
+					`${this.settings.clientId as string}:${
+						this.settings.clientSecret as string
+					}`,
 				).toString("base64")}`,
 			})
 			.body(
 				new URLSearchParams({
 					grant_type: "refresh_token",
 					refresh_token,
-					client_id: this.settings.clientId!,
+					client_id: this.settings.clientId as string,
 					redirect_uri: this.getRedirectUri(),
 					code_verifier: "challenge", // TODO: properly use PKCE challenge
 				}),
@@ -163,8 +170,11 @@ export default class TwitterConnection extends RefreshableConnection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo.data.id);
diff --git a/src/connections/Xbox/index.ts b/src/connections/Xbox/index.ts
index c592fd0b..935ff7ab 100644
--- a/src/connections/Xbox/index.ts
+++ b/src/connections/Xbox/index.ts
@@ -19,12 +19,12 @@
 import {
 	ConnectedAccount,
 	ConnectedAccountCommonOAuthTokenResponse,
+	Connection,
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
 } from "@spacebar/util";
 import wretch from "wretch";
-import Connection from "../../util/connections/Connection";
 import { XboxSettings } from "./XboxSettings";
 
 interface XboxUserResponse {
@@ -44,10 +44,10 @@ interface XboxUserResponse {
 	};
 }
 
-interface XboxErrorResponse {
-	error: string;
-	error_description: string;
-}
+// interface XboxErrorResponse {
+// 	error: string;
+// 	error_description: string;
+// }
 
 export default class XboxConnection extends Connection {
 	public readonly id = "xbox";
@@ -62,17 +62,20 @@ export default class XboxConnection extends Connection {
 	settings: XboxSettings = new XboxSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
+		const settings = ConnectionLoader.getConnectionConfig<XboxSettings>(
 			this.id,
 			this.settings,
-		) as XboxSettings;
+		);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("response_type", "code");
 		url.searchParams.append("scope", this.scopes.join(" "));
@@ -124,14 +127,16 @@ export default class XboxConnection extends Connection {
 				Accept: "application/json",
 				"Content-Type": "application/x-www-form-urlencoded",
 				Authorization: `Basic ${Buffer.from(
-					`${this.settings.clientId!}:${this.settings.clientSecret!}`,
+					`${this.settings.clientId as string}:${
+						this.settings.clientSecret as string
+					}`,
 				).toString("base64")}`,
 			})
 			.body(
 				new URLSearchParams({
 					grant_type: "authorization_code",
 					code: code,
-					client_id: this.settings.clientId!,
+					client_id: this.settings.clientId as string,
 					redirect_uri: this.getRedirectUri(),
 					scope: this.scopes.join(" "),
 				}),
@@ -174,8 +179,11 @@ export default class XboxConnection extends Connection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userToken = await this.getUserToken(tokenData.access_token);
 		const userInfo = await this.getUser(userToken);
 
diff --git a/src/connections/Youtube/index.ts b/src/connections/Youtube/index.ts
index f3a43fcc..844803cf 100644
--- a/src/connections/Youtube/index.ts
+++ b/src/connections/Youtube/index.ts
@@ -19,12 +19,12 @@
 import {
 	ConnectedAccount,
 	ConnectedAccountCommonOAuthTokenResponse,
+	Connection,
 	ConnectionCallbackSchema,
 	ConnectionLoader,
 	DiscordApiErrors,
 } from "@spacebar/util";
 import wretch from "wretch";
-import Connection from "../../util/connections/Connection";
 import { YoutubeSettings } from "./YoutubeSettings";
 
 interface YouTubeConnectionChannelListResult {
@@ -62,17 +62,20 @@ export default class YoutubeConnection extends Connection {
 	settings: YoutubeSettings = new YoutubeSettings();
 
 	init(): void {
-		this.settings = ConnectionLoader.getConnectionConfig(
+		const settings = ConnectionLoader.getConnectionConfig<YoutubeSettings>(
 			this.id,
 			this.settings,
-		) as YoutubeSettings;
+		);
+
+		if (settings.enabled && (!settings.clientId || !settings.clientSecret))
+			throw new Error(`Invalid settings for connection ${this.id}`);
 	}
 
 	getAuthorizationUrl(userId: string): string {
 		const state = this.createState(userId);
 		const url = new URL(this.authorizeUrl);
 
-		url.searchParams.append("client_id", this.settings.clientId!);
+		url.searchParams.append("client_id", this.settings.clientId as string);
 		url.searchParams.append("redirect_uri", this.getRedirectUri());
 		url.searchParams.append("response_type", "code");
 		url.searchParams.append("scope", this.scopes.join(" "));
@@ -101,8 +104,8 @@ export default class YoutubeConnection extends Connection {
 				new URLSearchParams({
 					grant_type: "authorization_code",
 					code: code,
-					client_id: this.settings.clientId!,
-					client_secret: this.settings.clientSecret!,
+					client_id: this.settings.clientId as string,
+					client_secret: this.settings.clientSecret as string,
 					redirect_uri: this.getRedirectUri(),
 				}),
 			)
@@ -131,8 +134,11 @@ export default class YoutubeConnection extends Connection {
 	async handleCallback(
 		params: ConnectionCallbackSchema,
 	): Promise<ConnectedAccount | null> {
-		const userId = this.getUserId(params.state);
-		const tokenData = await this.exchangeCode(params.state, params.code!);
+		const { state, code } = params;
+		if (!code) throw new Error("No code provided");
+
+		const userId = this.getUserId(state);
+		const tokenData = await this.exchangeCode(state, code);
 		const userInfo = await this.getUser(tokenData.access_token);
 
 		const exists = await this.hasConnection(userId, userInfo.items[0].id);
diff --git a/src/gateway/events/Close.ts b/src/gateway/events/Close.ts
index 572037af..16f6b188 100644
--- a/src/gateway/events/Close.ts
+++ b/src/gateway/events/Close.ts
@@ -54,11 +54,19 @@ export async function Close(this: WebSocket, code: number, reason: Buffer) {
 			status: "offline",
 		};
 
+		// TODO
+		// If a user was deleted, they may still be connected to gateway,
+		// which will cause this to throw when they disconnect.
+		// just send the ID of the user instead of the full correct payload for now
+		const userOrId = await User.getPublicUser(this.user_id).catch(() => ({
+			id: this.user_id,
+		}));
+
 		await emitEvent({
 			event: "PRESENCE_UPDATE",
 			user_id: this.user_id,
 			data: {
-				user: await User.getPublicUser(this.user_id),
+				user: userOrId,
 				activities: session.activities,
 				client_status: session?.client_info,
 				status: session.status,
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/Heartbeat.ts b/src/gateway/opcodes/Heartbeat.ts
index 7866c3e9..b9b62be3 100644
--- a/src/gateway/opcodes/Heartbeat.ts
+++ b/src/gateway/opcodes/Heartbeat.ts
@@ -25,5 +25,5 @@ export async function onHeartbeat(this: WebSocket) {
 
 	setHeartbeat(this);
 
-	await Send(this, { op: 11 });
+	await Send(this, { op: 11, d: {} });
 }
diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts
index 98fae3ed..7610901a 100644
--- a/src/gateway/opcodes/Identify.ts
+++ b/src/gateway/opcodes/Identify.ts
@@ -16,17 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { WebSocket, Payload } from "@spacebar/gateway";
+import {
+	WebSocket,
+	Payload,
+	setupListener,
+	Capabilities,
+	CLOSECODES,
+	OPCODES,
+	Send,
+} from "@spacebar/gateway";
 import {
 	checkToken,
 	Intents,
 	Member,
 	ReadyEventData,
-	User,
 	Session,
 	EVENTEnum,
 	Config,
-	PublicMember,
 	PublicUser,
 	PrivateUserProjection,
 	ReadState,
@@ -36,310 +42,385 @@ import {
 	PrivateSessionProjection,
 	MemberPrivateProjection,
 	PresenceUpdateEvent,
-	UserSettings,
 	IdentifySchema,
 	DefaultUserGuildSettings,
-	UserGuildSettings,
 	ReadyGuildDTO,
 	Guild,
-	UserTokenData,
-	ConnectedAccount,
+	PublicUserProjection,
+	ReadyUserGuildSettingsEntries,
+	UserSettings,
+	Permissions,
+	DMChannel,
+	GuildOrUnavailable,
+	Recipient,
+	OPCodes,
 } from "@spacebar/util";
-import { Send } from "../util/Send";
-import { CLOSECODES, OPCODES } from "../util/Constants";
-import { setupListener } from "../listener/listener";
-// import experiments from "./experiments.json";
-const experiments: unknown[] = [];
 import { check } from "./instanceOf";
-import { Recipient } from "@spacebar/util";
 
 // TODO: user sharding
 // TODO: check privileged intents, if defined in the config
-// TODO: check if already identified
-
-// TODO: Refactor identify ( and lazyrequest, tbh )
 
 export async function onIdentify(this: WebSocket, data: Payload) {
+	if (this.user_id) {
+		// we've already identified
+		return this.close(CLOSECODES.Already_authenticated);
+	}
+
 	clearTimeout(this.readyTimeout);
-	// TODO: is this needed now that we use `json-bigint`?
-	if (typeof data.d?.client_state?.highest_last_message_id === "number")
-		data.d.client_state.highest_last_message_id += "";
-	check.call(this, IdentifySchema, data.d);
 
+	// Check payload matches schema
+	check.call(this, IdentifySchema, data.d);
 	const identify: IdentifySchema = data.d;
 
-	let decoded: UserTokenData["decoded"];
-	try {
-		const { jwtSecret } = Config.get().security;
-		decoded = (await checkToken(identify.token, jwtSecret)).decoded; // will throw an error if invalid
-	} catch (error) {
-		console.error("invalid token", error);
-		return this.close(CLOSECODES.Authentication_failed);
-	}
-	this.user_id = decoded.id;
-	const session_id = this.session_id;
-
-	const [
-		user,
-		read_states,
-		members,
-		recipients,
-		session,
-		application,
-		connected_accounts,
-	] = await Promise.all([
-		User.findOneOrFail({
-			where: { id: this.user_id },
-			relations: ["relationships", "relationships.to", "settings"],
-			select: [...PrivateUserProjection, "relationships"],
-		}),
-		ReadState.find({ where: { user_id: this.user_id } }),
-		Member.find({
-			where: { id: this.user_id },
-			select: MemberPrivateProjection,
-			relations: [
-				"guild",
-				"guild.channels",
-				"guild.emojis",
-				"guild.roles",
-				"guild.stickers",
-				"user",
-				"roles",
-			],
-		}),
-		Recipient.find({
-			where: { user_id: this.user_id, closed: false },
-			relations: [
-				"channel",
-				"channel.recipients",
-				"channel.recipients.user",
-			],
-			// TODO: public user selection
-		}),
-		// save the session and delete it when the websocket is closed
-		Session.create({
-			user_id: this.user_id,
-			session_id: session_id,
-			// TODO: check if status is only one of: online, dnd, offline, idle
-			status: identify.presence?.status || "offline", //does the session always start as online?
-			client_info: {
-				//TODO read from identity
-				client: "desktop",
-				os: identify.properties?.os,
-				version: 0,
-			},
-			activities: [],
-		}).save(),
-		Application.findOne({ where: { id: this.user_id } }),
-		ConnectedAccount.find({ where: { user_id: this.user_id } }),
-	]);
+	this.capabilities = new Capabilities(identify.capabilities || 0);
 
+	const { user } = await checkToken(identify.token, {
+		relations: ["relationships", "relationships.to", "settings"],
+		select: [...PrivateUserProjection, "relationships"],
+	});
 	if (!user) return this.close(CLOSECODES.Authentication_failed);
-	if (!user.settings) {
-		user.settings = new UserSettings();
-		await user.settings.save();
-	}
+	this.user_id = user.id;
 
-	if (!identify.intents) identify.intents = BigInt("0x6ffffffff");
+	// Check intents
+	if (!identify.intents) identify.intents = 30064771071n; // TODO: what is this number?
 	this.intents = new Intents(identify.intents);
+
+	// TODO: actually do intent things.
+
+	// Validate sharding
 	if (identify.shard) {
 		this.shard_id = identify.shard[0];
 		this.shard_count = identify.shard[1];
+
 		if (
 			this.shard_count == null ||
 			this.shard_id == null ||
-			this.shard_id >= this.shard_count ||
+			this.shard_id > this.shard_count ||
 			this.shard_id < 0 ||
 			this.shard_count <= 0
 		) {
-			console.log(identify.shard);
+			// TODO: why do we even care about this right now?
+			console.log(
+				`[Gateway] Invalid sharding from ${user.id}: ${identify.shard}`,
+			);
 			return this.close(CLOSECODES.Invalid_shard);
 		}
 	}
-	let users: PublicUser[] = [];
 
-	const merged_members = members.map((x: Member) => {
+	// Generate a new gateway session ( id is already made, just save it in db )
+	const session = Session.create({
+		user_id: this.user_id,
+		session_id: this.session_id,
+		status: identify.presence?.status || "online",
+		client_info: {
+			client: identify.properties?.$device,
+			os: identify.properties?.os,
+			version: 0,
+		},
+		activities: identify.presence?.activities, // TODO: validation
+	});
+
+	// Get from database:
+	// * the users read states
+	// * guild members for this user
+	// * recipients ( dm channels )
+	// * the bot application, if it exists
+	const [, application, read_states, members, recipients] = await Promise.all(
+		[
+			session.save(),
+
+			Application.findOne({
+				where: { id: this.user_id },
+				select: ["id", "flags"],
+			}),
+
+			ReadState.find({
+				where: { user_id: this.user_id },
+				select: [
+					"id",
+					"channel_id",
+					"last_message_id",
+					"last_pin_timestamp",
+					"mention_count",
+				],
+			}),
+
+			Member.find({
+				where: { id: this.user_id },
+				select: {
+					// We only want some member props
+					...Object.fromEntries(
+						MemberPrivateProjection.map((x) => [x, true]),
+					),
+					settings: true, // guild settings
+					roles: { id: true }, // the full role is fetched from the `guild` relation
+
+					// TODO: we don't really need every property of
+					// guild channels, emoji, roles, stickers
+					// but we do want almost everything from guild.
+					// How do you do that without just enumerating the guild props?
+					guild: true,
+				},
+				relations: [
+					"guild",
+					"guild.channels",
+					"guild.emojis",
+					"guild.roles",
+					"guild.stickers",
+					"roles",
+
+					// For these entities, `user` is always just the logged in user we fetched above
+					// "user",
+				],
+			}),
+
+			Recipient.find({
+				where: { user_id: this.user_id, closed: false },
+				relations: [
+					"channel",
+					"channel.recipients",
+					"channel.recipients.user",
+				],
+				select: {
+					channel: {
+						id: true,
+						flags: true,
+						// is_spam: true,	// TODO
+						last_message_id: true,
+						last_pin_timestamp: true,
+						type: true,
+						icon: true,
+						name: true,
+						owner_id: true,
+						recipients: {
+							// we don't actually need this ID or any other information about the recipient info,
+							// but typeorm does not select anything from the users relation of recipients unless we select
+							// at least one column.
+							id: true,
+							// We only want public user data for each dm channel
+							user: Object.fromEntries(
+								PublicUserProjection.map((x) => [x, true]),
+							),
+						},
+					},
+				},
+			}),
+		],
+	);
+
+	// We forgot to migrate user settings from the JSON column of `users`
+	// to the `user_settings` table theyre in now,
+	// so for instances that migrated, users may not have a `user_settings` row.
+	if (!user.settings) {
+		user.settings = new UserSettings();
+		await user.settings.save();
+	}
+
+	// Generate merged_members
+	const merged_members = members.map((x) => {
 		return [
 			{
 				...x,
 				roles: x.roles.map((x) => x.id),
+
+				// add back user, which we don't fetch from db
+				// TODO: For guild profiles, this may need to be changed.
+				// TODO: The only field required in the user prop is `id`,
+				// but our types are annoying so I didn't bother.
+				user: user.toPublicUser(),
+
+				guild: {
+					id: x.guild.id,
+				},
 				settings: undefined,
-				guild: undefined,
 			},
 		];
-	}) as PublicMember[][];
-	// TODO: This type is bad.
-	let guilds: Partial<Guild>[] = members.map((x) => ({
-		...x.guild,
-		joined_at: x.joined_at,
-	}));
+	});
 
-	const pending_guilds: typeof guilds = [];
-	if (user.bot)
-		guilds = guilds.map((guild) => {
-			pending_guilds.push(guild);
-			return { id: guild.id, unavailable: true };
+	// Populated with guilds 'unavailable' currently
+	// Just for bots
+	const pending_guilds: Guild[] = [];
+
+	// Generate guilds list ( make them unavailable if user is bot )
+	const guilds: GuildOrUnavailable[] = members.map((member) => {
+		// filter guild channels we don't have permission to view
+		// TODO: check if this causes issues when the user is granted other roles?
+		member.guild.channels = member.guild.channels.filter((channel) => {
+			const perms = Permissions.finalPermission({
+				user: {
+					id: member.id,
+					roles: member.roles.map((x) => x.id),
+				},
+				guild: member.guild,
+				channel,
+			});
+
+			return perms.has("VIEW_CHANNEL");
 		});
 
-	// TODO: Rewrite this. Perhaps a DTO?
-	const user_guild_settings_entries = members.map((x) => ({
-		...DefaultUserGuildSettings,
-		...x.settings,
-		guild_id: x.guild.id,
-		channel_overrides: Object.entries(
-			x.settings.channel_overrides ?? {},
-		).map((y) => ({
-			...y[1],
-			channel_id: y[0],
-		})),
-	})) as unknown as UserGuildSettings[];
-
-	const channels = recipients.map((x) => {
-		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-		//@ts-ignore
-		x.channel.recipients = x.channel.recipients.map((x) =>
-			x.user.toPublicUser(),
-		);
-		//TODO is this needed? check if users in group dm that are not friends are sent in the READY event
-		users = users.concat(x.channel.recipients as unknown as User[]);
-		if (x.channel.isDm()) {
-			x.channel.recipients = x.channel.recipients?.filter(
-				(x) => x.id !== this.user_id,
-			);
+		if (user.bot) {
+			pending_guilds.push(member.guild);
+			return { id: member.guild.id, unavailable: true };
 		}
-		return x.channel;
-	});
 
-	for (const relation of user.relationships) {
-		const related_user = relation.to;
-		const public_related_user = {
-			username: related_user.username,
-			discriminator: related_user.discriminator,
-			id: related_user.id,
-			public_flags: related_user.public_flags,
-			avatar: related_user.avatar,
-			bot: related_user.bot,
-			bio: related_user.bio,
-			premium_since: user.premium_since,
-			premium_type: user.premium_type,
-			accent_color: related_user.accent_color,
+		return {
+			...member.guild.toJSON(),
+			joined_at: member.joined_at,
+
+			threads: [],
 		};
-		users.push(public_related_user);
-	}
+	});
+
+	// Generate user_guild_settings
+	const user_guild_settings_entries: ReadyUserGuildSettingsEntries[] =
+		members.map((x) => ({
+			...DefaultUserGuildSettings,
+			...x.settings,
+			guild_id: x.guild_id,
+			channel_overrides: Object.entries(
+				x.settings.channel_overrides ?? {},
+			).map((y) => ({
+				...y[1],
+				channel_id: y[0],
+			})),
+		}));
+
+	// Popultaed with users from private channels, relationships.
+	// Uses a set to dedupe for us.
+	const users: Set<PublicUser> = new Set();
+
+	// Generate dm channels from recipients list. Append recipients to `users` list
+	const channels = recipients
+		.filter(({ channel }) => channel.isDm())
+		.map((r) => {
+			// TODO: fix the types of Recipient
+			// Their channels are only ever private (I think) and thus are always DM channels
+			const channel = r.channel as DMChannel;
+
+			// Remove ourself from the list of other users in dm channel
+			channel.recipients = channel.recipients.filter(
+				(recipient) => recipient.user.id !== this.user_id,
+			);
+
+			const channelUsers = channel.recipients?.map((recipient) =>
+				recipient.user.toPublicUser(),
+			);
+
+			if (channelUsers && channelUsers.length > 0)
+				channelUsers.forEach((user) => users.add(user));
 
-	setImmediate(async () => {
-		// run in seperate "promise context" because ready payload is not dependent on those events
+			return {
+				id: channel.id,
+				flags: channel.flags,
+				last_message_id: channel.last_message_id,
+				type: channel.type,
+				recipients: channelUsers || [],
+				is_spam: false, // TODO
+			};
+		});
+
+	// From user relationships ( friends ), also append to `users` list
+	user.relationships.forEach((x) => users.add(x.to.toPublicUser()));
+
+	// Send SESSIONS_REPLACE and PRESENCE_UPDATE
+	const allSessions = (
+		await Session.find({
+			where: { user_id: this.user_id },
+			select: PrivateSessionProjection,
+		})
+	).map((x) => ({
+		// TODO how is active determined?
+		// in our lazy request impl, we just pick the 'most relevant' session
+		active: x.session_id == session.session_id,
+		activities: x.activities,
+		client_info: x.client_info,
+		// TODO: what does all mean?
+		session_id: x.session_id == session.session_id ? "all" : x.session_id,
+		status: x.status,
+	}));
+
+	Promise.all([
 		emitEvent({
 			event: "SESSIONS_REPLACE",
 			user_id: this.user_id,
-			data: await Session.find({
-				where: { user_id: this.user_id },
-				select: PrivateSessionProjection,
-			}),
-		} as SessionsReplace);
+			data: allSessions,
+		} as SessionsReplace),
 		emitEvent({
 			event: "PRESENCE_UPDATE",
 			user_id: this.user_id,
 			data: {
-				user: await User.getPublicUser(this.user_id),
+				user: user.toPublicUser(),
 				activities: session.activities,
-				client_status: session?.client_info,
+				client_status: session.client_info,
 				status: session.status,
 			},
-		} as PresenceUpdateEvent);
-	});
+		} as PresenceUpdateEvent),
+	]);
 
-	read_states.forEach((s: Partial<ReadState>) => {
-		s.id = s.channel_id;
-		delete s.user_id;
-		delete s.channel_id;
-	});
+	// Build READY
 
-	const privateUser = {
-		avatar: user.avatar,
-		mobile: user.mobile,
-		desktop: user.desktop,
-		discriminator: user.discriminator,
-		email: user.email,
-		flags: user.flags,
-		id: user.id,
-		mfa_enabled: user.mfa_enabled,
-		nsfw_allowed: user.nsfw_allowed,
-		phone: user.phone,
-		premium: user.premium,
-		premium_type: user.premium_type,
-		public_flags: user.public_flags,
-		premium_usage_flags: user.premium_usage_flags,
-		purchased_flags: user.purchased_flags,
-		username: user.username,
-		verified: user.verified,
-		bot: user.bot,
-		accent_color: user.accent_color,
-		banner: user.banner,
-		bio: user.bio,
-		premium_since: user.premium_since,
-	};
+	read_states.forEach((x) => {
+		x.id = x.channel_id;
+	});
 
 	const d: ReadyEventData = {
 		v: 9,
-		application: {
-			id: application?.id ?? "",
-			flags: application?.flags ?? 0,
-		}, //TODO: check this code!
-		user: privateUser,
+		application: application
+			? { id: application.id, flags: application.flags }
+			: undefined,
+		user: user.toPrivateUser(),
 		user_settings: user.settings,
-		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-		// @ts-ignore
-		guilds: guilds.map((x: Guild & { joined_at: Date }) => {
-			return {
-				...new ReadyGuildDTO(x).toJSON(),
-				guild_hashes: {},
-				joined_at: x.joined_at,
-				name: x.name,
-				icon: x.icon,
-			};
-		}),
-		guild_experiments: [], // TODO
-		geo_ordered_rtc_regions: [], // TODO
+		guilds: this.capabilities.has(Capabilities.FLAGS.CLIENT_STATE_V2)
+			? guilds.map((x) => new ReadyGuildDTO(x).toJSON())
+			: guilds,
 		relationships: user.relationships.map((x) => x.toPublicRelationship()),
 		read_state: {
 			entries: read_states,
 			partial: false,
-			version: 304128,
+			version: 0, // TODO
 		},
 		user_guild_settings: {
 			entries: user_guild_settings_entries,
-			partial: false, // TODO partial
-			version: 642,
+			partial: false,
+			version: 0, // TODO
 		},
 		private_channels: channels,
-		session_id: session_id,
-		analytics_token: "", // TODO
-		connected_accounts,
-		consents: {
-			personalization: {
-				consented: false, // TODO
-			},
-		},
-		country_code: user.settings.locale,
-		friend_suggestion_count: 0, // TODO
-		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-		// @ts-ignore
-		experiments: experiments, // TODO
-		guild_join_requests: [], // TODO what is this?
-		users: users.filter((x) => x).unique(),
+		session_id: this.session_id,
+		country_code: user.settings.locale, // TODO: do ip analysis instead
+		users: Array.from(users),
 		merged_members: merged_members,
-		// shard // TODO: only for user sharding
-		sessions: [], // TODO:
+		sessions: allSessions,
+
+		resume_gateway_url:
+			Config.get().gateway.endpointClient ||
+			Config.get().gateway.endpointPublic ||
+			"ws://127.0.0.1:3001",
 
 		// lol hack whatever
 		required_action:
 			Config.get().login.requireVerification && !user.verified
 				? "REQUIRE_VERIFIED_EMAIL"
 				: undefined,
+
+		consents: {
+			personalization: {
+				consented: false, // TODO
+			},
+		},
+		experiments: [],
+		guild_join_requests: [],
+		connected_accounts: [],
+		guild_experiments: [],
+		geo_ordered_rtc_regions: [],
+		api_code_version: 1,
+		friend_suggestion_count: 0,
+		analytics_token: "",
+		tutorial: null,
+		session_type: "normal", // TODO
+		auth_session_id_hash: "", // TODO
 	};
 
-	// TODO: send real proper data structure
+	// Send READY
 	await Send(this, {
 		op: OPCODES.Dispatch,
 		t: EVENTEnum.Ready,
@@ -347,23 +428,41 @@ export async function onIdentify(this: WebSocket, data: Payload) {
 		d,
 	});
 
+	// If we're a bot user, send GUILD_CREATE for each unavailable guild
 	await Promise.all(
-		pending_guilds.map((guild) =>
+		pending_guilds.map((x) =>
 			Send(this, {
 				op: OPCODES.Dispatch,
 				t: EVENTEnum.GuildCreate,
 				s: this.sequence++,
-				d: guild,
-			})?.catch(console.error),
+				d: x,
+			})?.catch((e) =>
+				console.error(`[Gateway] error when sending bot guilds`, e),
+			),
 		),
 	);
 
-	//TODO send READY_SUPPLEMENTAL
+	// TODO: ready supplemental
+	await Send(this, {
+		op: OPCodes.DISPATCH,
+		t: EVENTEnum.ReadySupplemental,
+		s: this.sequence++,
+		d: {
+			merged_presences: {
+				guilds: [],
+				friends: [],
+			},
+			// these merged members seem to be all users currently in vc in your guilds
+			merged_members: [],
+			lazy_private_channels: [],
+			guilds: [], // { voice_states: [], id: string, embedded_activities: [] }
+			// embedded_activities are users currently in an activity?
+			disclose: [], // Config.get().general.uniqueUsernames ? ["pomelo"] : []
+		},
+	});
+
 	//TODO send GUILD_MEMBER_LIST_UPDATE
-	//TODO send SESSIONS_REPLACE
 	//TODO send VOICE_STATE_UPDATE to let the client know if another device is already connected to a voice channel
 
 	await setupListener.call(this);
-
-	// console.log(`${this.ipAddress} identified as ${d.user.id}`);
 }
diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts
index 64e50d92..4ad1ae7b 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
@@ -92,7 +95,7 @@ async function getMembers(guild_id: string, range: [number, number]) {
 		console.error(`LazyRequest`, e);
 	}
 
-	if (!members) {
+	if (!members || !members.length) {
 		return {
 			items: [],
 			groups: [],
@@ -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/gateway/util/Capabilities.ts b/src/gateway/util/Capabilities.ts
new file mode 100644
index 00000000..6c94bb45
--- /dev/null
+++ b/src/gateway/util/Capabilities.ts
@@ -0,0 +1,26 @@
+import { BitField, BitFieldResolvable, BitFlag } from "@spacebar/util";
+
+export type CapabilityResolvable = BitFieldResolvable | CapabilityString;
+type CapabilityString = keyof typeof Capabilities.FLAGS;
+
+export class Capabilities extends BitField {
+	static FLAGS = {
+		// Thanks, Opencord!
+		// https://github.com/MateriiApps/OpenCord/blob/master/app/src/main/java/com/xinto/opencord/gateway/io/Capabilities.kt
+		LAZY_USER_NOTES: BitFlag(0),
+		NO_AFFINE_USER_IDS: BitFlag(1),
+		VERSIONED_READ_STATES: BitFlag(2),
+		VERSIONED_USER_GUILD_SETTINGS: BitFlag(3),
+		DEDUPLICATE_USER_OBJECTS: BitFlag(4),
+		PRIORITIZED_READY_PAYLOAD: BitFlag(5),
+		MULTIPLE_GUILD_EXPERIMENT_POPULATIONS: BitFlag(6),
+		NON_CHANNEL_READ_STATES: BitFlag(7),
+		AUTH_TOKEN_REFRESH: BitFlag(8),
+		USER_SETTINGS_PROTO: BitFlag(9),
+		CLIENT_STATE_V2: BitFlag(10),
+		PASSIVE_GUILD_UPDATE: BitFlag(11),
+	};
+
+	any = (capability: CapabilityResolvable) => super.any(capability);
+	has = (capability: CapabilityResolvable) => super.has(capability);
+}
diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts
index 972129c7..833756ff 100644
--- a/src/gateway/util/WebSocket.ts
+++ b/src/gateway/util/WebSocket.ts
@@ -19,6 +19,7 @@
 import { Intents, ListenEventOpts, Permissions } from "@spacebar/util";
 import WS from "ws";
 import { Deflate, Inflate } from "fast-zlib";
+import { Capabilities } from "./Capabilities";
 // import { Client } from "@spacebar/webrtc";
 
 export interface WebSocket extends WS {
@@ -40,5 +41,6 @@ export interface WebSocket extends WS {
 	events: Record<string, undefined | (() => unknown)>;
 	member_events: Record<string, () => unknown>;
 	listen_options: ListenEventOpts;
+	capabilities?: Capabilities;
 	// client?: Client;
 }
diff --git a/src/gateway/util/index.ts b/src/gateway/util/index.ts
index 627f12b2..6ef694d9 100644
--- a/src/gateway/util/index.ts
+++ b/src/gateway/util/index.ts
@@ -21,3 +21,4 @@ export * from "./Send";
 export * from "./SessionUtils";
 export * from "./Heartbeat";
 export * from "./WebSocket";
+export * from "./Capabilities";
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/connections/Connection.ts b/src/util/connections/Connection.ts
index becee589..5bdebd47 100644
--- a/src/util/connections/Connection.ts
+++ b/src/util/connections/Connection.ts
@@ -24,7 +24,7 @@ import { Config, DiscordApiErrors } from "../util";
 /**
  * A connection that can be used to connect to an external service.
  */
-export default abstract class Connection {
+export abstract class Connection {
 	id: string;
 	settings: { enabled: boolean };
 	states: Map<string, string> = new Map();
diff --git a/src/util/connections/ConnectionLoader.ts b/src/util/connections/ConnectionLoader.ts
index 28f1a202..e9dc6973 100644
--- a/src/util/connections/ConnectionLoader.ts
+++ b/src/util/connections/ConnectionLoader.ts
@@ -16,9 +16,9 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { Connection } from "@spacebar/util";
 import fs from "fs";
 import path from "path";
-import Connection from "./Connection";
 import { ConnectionConfig } from "./ConnectionConfig";
 import { ConnectionStore } from "./ConnectionStore";
 
@@ -48,8 +48,7 @@ export class ConnectionLoader {
 		});
 	}
 
-	// eslint-disable-next-line @typescript-eslint/no-explicit-any
-	public static getConnectionConfig(id: string, defaults?: any): any {
+	public static getConnectionConfig<T>(id: string, defaults?: unknown): T {
 		let cfg = ConnectionConfig.get()[id];
 		if (defaults) {
 			if (cfg) cfg = Object.assign({}, defaults, cfg);
@@ -70,8 +69,7 @@ export class ConnectionLoader {
 
 	public static async setConnectionConfig(
 		id: string,
-		// eslint-disable-next-line @typescript-eslint/no-explicit-any
-		config: Partial<any>,
+		config: Partial<unknown>,
 	): Promise<void> {
 		if (!config)
 			console.warn(`[Connections/WARN] ${id} tried to set config=null!`);
diff --git a/src/util/connections/ConnectionStore.ts b/src/util/connections/ConnectionStore.ts
index 39abfea6..95e54fd9 100644
--- a/src/util/connections/ConnectionStore.ts
+++ b/src/util/connections/ConnectionStore.ts
@@ -16,8 +16,8 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import Connection from "./Connection";
-import RefreshableConnection from "./RefreshableConnection";
+import { Connection } from "./Connection";
+import { RefreshableConnection } from "./RefreshableConnection";
 
 export class ConnectionStore {
 	public static connections: Map<string, Connection | RefreshableConnection> =
diff --git a/src/util/connections/RefreshableConnection.ts b/src/util/connections/RefreshableConnection.ts
index fd93adfa..88ad8dab 100644
--- a/src/util/connections/RefreshableConnection.ts
+++ b/src/util/connections/RefreshableConnection.ts
@@ -18,13 +18,14 @@
 
 import { ConnectedAccount } from "../entities";
 import { ConnectedAccountCommonOAuthTokenResponse } from "../interfaces";
-import Connection from "./Connection";
+import { Connection } from "./Connection";
 
 /**
  * A connection that can refresh its token.
  */
-export default abstract class RefreshableConnection extends Connection {
+export abstract class RefreshableConnection extends Connection {
 	refreshEnabled = true;
+
 	/**
 	 * Refreshes the token for a connected account.
 	 * @param connectedAccount The connected account to refresh
diff --git a/src/util/dtos/ConnectedAccountDTO.ts b/src/util/dtos/ConnectedAccountDTO.ts
index 0a3604d5..f9efd980 100644
--- a/src/util/dtos/ConnectedAccountDTO.ts
+++ b/src/util/dtos/ConnectedAccountDTO.ts
@@ -30,7 +30,7 @@ export class ConnectedAccountDTO {
 	verified?: boolean;
 	visibility?: number;
 	integrations?: string[];
-	metadata_?: any;
+	metadata_?: unknown;
 	metadata_visibility?: number;
 	two_way_link?: boolean;
 
diff --git a/src/util/dtos/ReadyGuildDTO.ts b/src/util/dtos/ReadyGuildDTO.ts
index b21afe74..905ede74 100644
--- a/src/util/dtos/ReadyGuildDTO.ts
+++ b/src/util/dtos/ReadyGuildDTO.ts
@@ -18,13 +18,45 @@
 
 import {
 	Channel,
+	ChannelOverride,
+	ChannelType,
 	Emoji,
 	Guild,
-	PublicMember,
+	PublicUser,
 	Role,
 	Sticker,
+	UserGuildSettings,
+	PublicMember,
 } from "../entities";
 
+// TODO: this is not the best place for this type
+export type ReadyUserGuildSettingsEntries = Omit<
+	UserGuildSettings,
+	"channel_overrides"
+> & {
+	channel_overrides: (ChannelOverride & { channel_id: string })[];
+};
+
+// TODO: probably should move somewhere else
+export interface ReadyPrivateChannel {
+	id: string;
+	flags: number;
+	is_spam: boolean;
+	last_message_id?: string;
+	recipients: PublicUser[];
+	type: ChannelType.DM | ChannelType.GROUP_DM;
+}
+
+export type GuildOrUnavailable =
+	| { id: string; unavailable: boolean }
+	| (Guild & { joined_at?: Date; unavailable: undefined });
+
+const guildIsAvailable = (
+	guild: GuildOrUnavailable,
+): guild is Guild & { joined_at: Date; unavailable: false } => {
+	return guild.unavailable != true;
+};
+
 export interface IReadyGuildDTO {
 	application_command_counts?: { 1: number; 2: number; 3: number }; // ????????????
 	channels: Channel[];
@@ -65,12 +97,21 @@ export interface IReadyGuildDTO {
 		max_members: number | undefined;
 		nsfw_level: number | undefined;
 		hub_type?: unknown | null; // ????
+
+		home_header: null; // TODO
+		latest_onboarding_question_id: null; // TODO
+		safety_alerts_channel_id: null; // TODO
+		max_stage_video_channel_users: 50; // TODO
+		nsfw: boolean;
+		id: string;
 	};
 	roles: Role[];
 	stage_instances: unknown[];
 	stickers: Sticker[];
 	threads: unknown[];
 	version: string;
+	guild_hashes: unknown;
+	unavailable: boolean;
 }
 
 export class ReadyGuildDTO implements IReadyGuildDTO {
@@ -113,14 +154,30 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
 		max_members: number | undefined;
 		nsfw_level: number | undefined;
 		hub_type?: unknown | null; // ????
+
+		home_header: null; // TODO
+		latest_onboarding_question_id: null; // TODO
+		safety_alerts_channel_id: null; // TODO
+		max_stage_video_channel_users: 50; // TODO
+		nsfw: boolean;
+		id: string;
 	};
 	roles: Role[];
 	stage_instances: unknown[];
 	stickers: Sticker[];
 	threads: unknown[];
 	version: string;
+	guild_hashes: unknown;
+	unavailable: boolean;
+	joined_at: Date;
+
+	constructor(guild: GuildOrUnavailable) {
+		if (!guildIsAvailable(guild)) {
+			this.id = guild.id;
+			this.unavailable = true;
+			return;
+		}
 
-	constructor(guild: Guild) {
 		this.application_command_counts = {
 			1: 5,
 			2: 2,
@@ -164,12 +221,21 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
 			max_members: guild.max_members,
 			nsfw_level: guild.nsfw_level,
 			hub_type: null,
+
+			home_header: null,
+			id: guild.id,
+			latest_onboarding_question_id: null,
+			max_stage_video_channel_users: 50, // TODO
+			nsfw: guild.nsfw,
+			safety_alerts_channel_id: null,
 		};
 		this.roles = guild.roles;
 		this.stage_instances = [];
 		this.stickers = guild.stickers;
 		this.threads = [];
 		this.version = "1"; // ??????
+		this.guild_hashes = {};
+		this.joined_at = guild.joined_at;
 	}
 
 	toJSON() {
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index 9ce04848..19952bc2 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { HTTPError } from "lambert-server";
 import {
 	Column,
 	Entity,
@@ -24,26 +25,25 @@ import {
 	OneToMany,
 	RelationId,
 } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-import { PublicUserProjection, User } from "./User";
-import { HTTPError } from "lambert-server";
+import { DmChannelDTO } from "../dtos";
+import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
 import {
+	InvisibleCharacters,
+	Snowflake,
 	containsAll,
 	emitEvent,
 	getPermission,
-	Snowflake,
 	trimSpecial,
-	InvisibleCharacters,
 } from "../util";
-import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
-import { Recipient } from "./Recipient";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Invite } from "./Invite";
 import { Message } from "./Message";
 import { ReadState } from "./ReadState";
-import { Invite } from "./Invite";
+import { Recipient } from "./Recipient";
+import { PublicUserProjection, User } from "./User";
 import { VoiceState } from "./VoiceState";
 import { Webhook } from "./Webhook";
-import { DmChannelDTO } from "../dtos";
 
 export enum ChannelType {
 	GUILD_TEXT = 0, // a text channel within a guild
@@ -302,8 +302,10 @@ export class Channel extends BaseClass {
 					: channel.position) || 0,
 		};
 
+		const ret = Channel.create(channel);
+
 		await Promise.all([
-			Channel.create(channel).save(),
+			ret.save(),
 			!opts?.skipEventEmit
 				? emitEvent({
 						event: "CHANNEL_CREATE",
@@ -313,7 +315,7 @@ export class Channel extends BaseClass {
 				: Promise.resolve(),
 		]);
 
-		return channel;
+		return ret;
 	}
 
 	static async createDMChannel(
@@ -468,6 +470,18 @@ export class Channel extends BaseClass {
 		];
 		return disallowedChannelTypes.indexOf(this.type) == -1;
 	}
+
+	toJSON() {
+		return {
+			...this,
+
+			// these fields are not returned depending on the type of channel
+			bitrate: this.bitrate || undefined,
+			user_limit: this.user_limit || undefined,
+			rate_limit_per_user: this.rate_limit_per_user || undefined,
+			owner_id: this.owner_id || undefined,
+		};
+	}
 }
 
 export interface ChannelPermissionOverwrite {
@@ -482,3 +496,33 @@ export enum ChannelPermissionOverwriteType {
 	member = 1,
 	group = 2,
 }
+
+export interface DMChannel extends Omit<Channel, "type" | "recipients"> {
+	type: ChannelType.DM | ChannelType.GROUP_DM;
+	recipients: Recipient[];
+}
+
+// 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/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts
index 5dd21250..6e089de1 100644
--- a/src/util/entities/ConnectedAccount.ts
+++ b/src/util/entities/ConnectedAccount.ts
@@ -66,6 +66,7 @@ export class ConnectedAccount extends BaseClass {
 	integrations?: string[] = [];
 
 	@Column({ type: "simple-json", name: "metadata", nullable: true })
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 	metadata_?: any;
 
 	@Column()
diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
index e8454986..e364ed98 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
@@ -353,6 +353,7 @@ export class Guild extends BaseClass {
 			position: 0,
 			icon: undefined,
 			unicode_emoji: undefined,
+			flags: 0, // TODO?
 		}).save();
 
 		if (!body.channels || !body.channels.length)
@@ -389,4 +390,11 @@ export class Guild extends BaseClass {
 
 		return guild;
 	}
+
+	toJSON() {
+		return {
+			...this,
+			unavailable: this.unavailable == false ? undefined : true,
+		};
+	}
 }
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/Message.ts b/src/util/entities/Message.ts
index 519c431e..3598d29f 100644
--- a/src/util/entities/Message.ts
+++ b/src/util/entities/Message.ts
@@ -193,7 +193,7 @@ export class Message extends BaseClass {
 	};
 
 	@Column({ nullable: true })
-	flags?: string;
+	flags?: number;
 
 	@Column({ type: "simple-json", nullable: true })
 	message_reference?: {
@@ -217,6 +217,29 @@ export class Message extends BaseClass {
 
 	@Column({ type: "simple-json", nullable: true })
 	components?: MessageComponent[];
+
+	toJSON(): Message {
+		return {
+			...this,
+			author_id: undefined,
+			member_id: undefined,
+			webhook_id: undefined,
+			application_id: undefined,
+
+			nonce: this.nonce ?? undefined,
+			tts: this.tts ?? false,
+			guild: this.guild ?? undefined,
+			webhook: this.webhook ?? undefined,
+			interaction: this.interaction ?? undefined,
+			reactions: this.reactions ?? undefined,
+			sticker_items: this.sticker_items ?? undefined,
+			message_reference: this.message_reference ?? undefined,
+			author: this.author?.toPublicUser() ?? undefined,
+			activity: this.activity ?? undefined,
+			application: this.application ?? undefined,
+			components: this.components ?? undefined,
+		};
+	}
 }
 
 export interface MessageComponent {
diff --git a/src/util/entities/Role.ts b/src/util/entities/Role.ts
index 85877c12..9a601f31 100644
--- a/src/util/entities/Role.ts
+++ b/src/util/entities/Role.ts
@@ -66,4 +66,7 @@ export class Role extends BaseClass {
 		integration_id?: string;
 		premium_subscriber?: boolean;
 	};
+
+	@Column({ default: 0 })
+	flags: number;
 }
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index df9af328..3f1bda05 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
@@ -174,7 +175,7 @@ export class User extends BaseClass {
 	email?: string; // email of the user
 
 	@Column()
-	flags: string = "0"; // UserFlags // TODO: generate
+	flags: number = 0; // UserFlags // TODO: generate
 
 	@Column()
 	public_flags: number = 0;
@@ -280,6 +281,15 @@ export class User extends BaseClass {
 		return user as PublicUser;
 	}
 
+	toPrivateUser() {
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		const user: any = {};
+		PrivateUserProjection.forEach((x) => {
+			user[x] = this[x];
+		});
+		return user as UserPrivate;
+	}
+
 	static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
 		return await User.findOneOrFail({
 			where: { id: user_id },
@@ -381,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/Event.ts b/src/util/interfaces/Event.ts
index 76a5f8d0..deb54428 100644
--- a/src/util/interfaces/Event.ts
+++ b/src/util/interfaces/Event.ts
@@ -28,7 +28,6 @@ import {
 	Role,
 	Emoji,
 	PublicMember,
-	UserGuildSettings,
 	Guild,
 	Channel,
 	PublicUser,
@@ -40,6 +39,10 @@ import {
 	UserSettings,
 	IReadyGuildDTO,
 	ReadState,
+	UserPrivate,
+	ReadyUserGuildSettingsEntries,
+	ReadyPrivateChannel,
+	GuildOrUnavailable,
 } from "@spacebar/util";
 
 export interface Event {
@@ -68,22 +71,10 @@ export interface PublicRelationship {
 
 export interface ReadyEventData {
 	v: number;
-	user: PublicUser & {
-		mobile: boolean;
-		desktop: boolean;
-		email: string | undefined;
-		flags: string;
-		mfa_enabled: boolean;
-		nsfw_allowed: boolean;
-		phone: string | undefined;
-		premium: boolean;
-		premium_type: number;
-		verified: boolean;
-		bot: boolean;
-	};
-	private_channels: Channel[]; // this will be empty for bots
+	user: UserPrivate;
+	private_channels: ReadyPrivateChannel[]; // this will be empty for bots
 	session_id: string; // resuming
-	guilds: IReadyGuildDTO[];
+	guilds: IReadyGuildDTO[] | GuildOrUnavailable[]; // depends on capability
 	analytics_token?: string;
 	connected_accounts?: ConnectedAccount[];
 	consents?: {
@@ -115,7 +106,7 @@ export interface ReadyEventData {
 		version: number;
 	};
 	user_guild_settings?: {
-		entries: UserGuildSettings[];
+		entries: ReadyUserGuildSettingsEntries[];
 		version: number;
 		partial: boolean;
 	};
@@ -127,6 +118,17 @@ export interface ReadyEventData {
 	// probably all users who the user is in contact with
 	users?: PublicUser[];
 	sessions: unknown[];
+	api_code_version: number;
+	tutorial: number | null;
+	resume_gateway_url: string;
+	session_type: string;
+	auth_session_id_hash: string;
+	required_action?:
+		| "REQUIRE_VERIFIED_EMAIL"
+		| "REQUIRE_VERIFIED_PHONE"
+		| "REQUIRE_CAPTCHA" // TODO: allow these to be triggered
+		| "TOS_UPDATE_ACKNOWLEDGMENT"
+		| "AGREEMENTS";
 }
 
 export interface ReadyEvent extends Event {
@@ -581,6 +583,7 @@ export type EventData =
 
 export enum EVENTEnum {
 	Ready = "READY",
+	ReadySupplemental = "READY_SUPPLEMENTAL",
 	ChannelCreate = "CHANNEL_CREATE",
 	ChannelUpdate = "CHANNEL_UPDATE",
 	ChannelDelete = "CHANNEL_DELETE",
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/ConnectedAccountSchema.ts b/src/util/schemas/ConnectedAccountSchema.ts
index fe808a35..5fd05b71 100644
--- a/src/util/schemas/ConnectedAccountSchema.ts
+++ b/src/util/schemas/ConnectedAccountSchema.ts
@@ -30,7 +30,7 @@ export interface ConnectedAccountSchema {
 	verified?: boolean;
 	visibility?: number;
 	integrations?: string[];
-	metadata_?: any;
+	metadata_?: unknown;
 	metadata_visibility?: number;
 	two_way_link?: boolean;
 }
diff --git a/src/util/schemas/ConnectionCallbackSchema.ts b/src/util/schemas/ConnectionCallbackSchema.ts
index eb86c087..b66bfe20 100644
--- a/src/util/schemas/ConnectionCallbackSchema.ts
+++ b/src/util/schemas/ConnectionCallbackSchema.ts
@@ -21,5 +21,5 @@ export interface ConnectionCallbackSchema {
 	state: string;
 	insecure: boolean;
 	friend_sync: boolean;
-	openid_params?: any; // TODO: types
+	openid_params?: unknown; // TODO: types
 }
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/MessageCreateSchema.ts b/src/util/schemas/MessageCreateSchema.ts
index 45cd735e..7e130751 100644
--- a/src/util/schemas/MessageCreateSchema.ts
+++ b/src/util/schemas/MessageCreateSchema.ts
@@ -29,7 +29,7 @@ export interface MessageCreateSchema {
 	nonce?: string;
 	channel_id?: string;
 	tts?: boolean;
-	flags?: string;
+	flags?: number;
 	embeds?: Embed[];
 	embed?: Embed;
 	// TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object)
diff --git a/src/util/schemas/RegisterSchema.ts b/src/util/schemas/RegisterSchema.ts
index f6c99b18..7b7de9c7 100644
--- a/src/util/schemas/RegisterSchema.ts
+++ b/src/util/schemas/RegisterSchema.ts
@@ -42,4 +42,8 @@ export interface RegisterSchema {
 	captcha_key?: string;
 
 	promotional_email_opt_in?: boolean;
+
+	// part of pomelo
+	unique_username_registration?: boolean;
+	global_name?: string;
 }
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/AutoUpdate.ts b/src/util/util/AutoUpdate.ts
index 1f90a41e..2af5cf1c 100644
--- a/src/util/util/AutoUpdate.ts
+++ b/src/util/util/AutoUpdate.ts
@@ -18,7 +18,7 @@
 
 import "missing-native-js-functions";
 import fetch from "node-fetch";
-import ProxyAgent from "proxy-agent";
+import { ProxyAgent } from "proxy-agent";
 import readline from "readline";
 import fs from "fs/promises";
 import path from "path";
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/JSON.ts b/src/util/util/JSON.ts
index 1c39b66e..c7dcf47e 100644
--- a/src/util/util/JSON.ts
+++ b/src/util/util/JSON.ts
@@ -27,6 +27,16 @@ const JSONReplacer = function (
 		return (this[key] as Date).toISOString().replace("Z", "+00:00");
 	}
 
+	// erlpack encoding doesn't call json.stringify,
+	// so our toJSON functions don't get called.
+	// manually call it here
+	// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+	//@ts-ignore
+	if (this?.[key]?.toJSON)
+		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+		//@ts-ignore
+		this[key] = this[key].toJSON();
+
 	return value;
 };
 
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index 90310176..97bdec74 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -19,94 +19,69 @@
 import jwt, { VerifyOptions } from "jsonwebtoken";
 import { Config } from "./Config";
 import { User } from "../entities";
+// TODO: dont use deprecated APIs lol
+import {
+	FindOptionsRelationByString,
+	FindOptionsSelectByString,
+} from "typeorm";
 
 export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
 
 export type UserTokenData = {
 	user: User;
-	decoded: { id: string; iat: number };
+	decoded: { id: string; iat: number; email?: string };
 };
 
-async function checkEmailToken(
-	decoded: jwt.JwtPayload,
-): Promise<UserTokenData> {
-	// eslint-disable-next-line no-async-promise-executor
-	return new Promise(async (res, rej) => {
-		if (!decoded.iat) return rej("Invalid Token"); // will never happen, just for typings.
-
-		const user = await User.findOne({
-			where: {
-				email: decoded.email,
-			},
-			select: [
-				"email",
-				"id",
-				"verified",
-				"deleted",
-				"disabled",
-				"username",
-				"data",
-			],
-		});
-
-		if (!user) return rej("Invalid Token");
-
-		if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000)
-			return rej("Invalid Token");
-
-		// Using as here because we assert `id` and `iat` are in decoded.
-		// TS just doesn't want to assume its there, though.
-		return res({ decoded, user } as UserTokenData);
-	});
-}
-
-export function checkToken(
+export const checkToken = (
 	token: string,
-	jwtSecret: string,
-	isEmailVerification = false,
-): Promise<UserTokenData> {
-	return new Promise((res, rej) => {
-		token = token.replace("Bot ", "");
-		token = token.replace("Bearer ", "");
-		/**
-		in spacebar, even with instances that have bot distinction; we won't enforce "Bot" prefix,
-		as we don't really have separate pathways for bots 
-		**/
-
-		jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded) => {
-			if (err || !decoded) return rej("Invalid Token");
-			if (
-				typeof decoded == "string" ||
-				!("id" in decoded) ||
-				!decoded.iat
-			)
-				return rej("Invalid Token"); // will never happen, just for typings.
-
-			if (isEmailVerification) return res(checkEmailToken(decoded));
-
-			const user = await User.findOne({
-				where: { id: decoded.id },
-				select: ["data", "bot", "disabled", "deleted", "rights"],
-			});
-
-			if (!user) return rej("Invalid Token");
-
-			// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
-			if (
-				decoded.iat * 1000 <
-				new Date(user.data.valid_tokens_since).setSeconds(0, 0)
-			)
-				return rej("Invalid Token");
-
-			if (user.disabled) return rej("User disabled");
-			if (user.deleted) return rej("User not found");
-
-			// Using as here because we assert `id` and `iat` are in decoded.
-			// TS just doesn't want to assume its there, though.
-			return res({ decoded, user } as UserTokenData);
-		});
+	opts?: {
+		select?: FindOptionsSelectByString<User>;
+		relations?: FindOptionsRelationByString;
+	},
+): Promise<UserTokenData> =>
+	new Promise((resolve, reject) => {
+		token = token.replace("Bot ", ""); // there is no bot distinction in sb
+		token = token.replace("Bearer ", ""); // allow bearer tokens
+
+		jwt.verify(
+			token,
+			Config.get().security.jwtSecret,
+			JWTOptions,
+			async (err, out) => {
+				const decoded = out as UserTokenData["decoded"];
+				if (err || !decoded) return reject("Invalid Token");
+
+				const user = await User.findOne({
+					where: decoded.email
+						? { email: decoded.email }
+						: { id: decoded.id },
+					select: [
+						...(opts?.select || []),
+						"bot",
+						"disabled",
+						"deleted",
+						"rights",
+						"data",
+					],
+					relations: opts?.relations,
+				});
+
+				if (!user) return reject("User not found");
+
+				// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
+				if (
+					decoded.iat * 1000 <
+					new Date(user.data.valid_tokens_since).setSeconds(0, 0)
+				)
+					return reject("Invalid Token");
+
+				if (user.disabled) return reject("User disabled");
+				if (user.deleted) return reject("User not found");
+
+				return resolve({ decoded, user });
+			},
+		);
 	});
-}
 
 export async function generateToken(id: string, email?: string) {
 	const iat = Math.floor(Date.now() / 1000);
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,