diff --git a/src/database/guild.rs b/src/database/guild.rs
index 5ae9bc27941279151c70be669ef88614e3118915..bbce3b17650189cff05c8a212be5ccda8ed1884b 100644
--- a/src/database/guild.rs
+++ b/src/database/guild.rs
@@ -16,8 +16,8 @@ pub struct Member {
 
 #[derive(Serialize, Deserialize, Debug)]
 pub struct Invite {
-    pub id: String,
-    pub custom: bool,
+    pub code: String,
+    pub creator: String,
     pub channel: String,
 }
 
diff --git a/src/database/permissions.rs b/src/database/permissions.rs
index 3b34c020d31cec60d3141328425acbc6db1afe6f..29ebdbfd9b97f72206eedbabd8ba4610b520616a 100644
--- a/src/database/permissions.rs
+++ b/src/database/permissions.rs
@@ -137,7 +137,7 @@ impl PermissionCalculator {
         };
 
         if let Some(guild) = &guild {
-            self.member = get_member(guild, &self.user.id);
+            self.member = get_member(&guild.id, &self.user.id);
         }
 
         self.guild = guild;
diff --git a/src/guards/channel.rs b/src/guards/channel.rs
index a05db4f90188225bf7a65eed939d3acfa939b835..ace30967f9663c99379c3c3472445c0d002384fd 100644
--- a/src/guards/channel.rs
+++ b/src/guards/channel.rs
@@ -16,6 +16,7 @@ pub struct ChannelRef {
     #[serde(rename = "type")]
     pub channel_type: u8,
 
+    pub name: Option<String>,
     pub last_message: Option<LastMessage>,
 
     // information required for permission calculations
@@ -25,6 +26,31 @@ pub struct ChannelRef {
 }
 
 impl ChannelRef {
+    pub fn from(id: String) -> Option<ChannelRef> {
+        match database::get_collection("channels").find_one(
+            doc! { "_id": id },
+            FindOneOptions::builder()
+                .projection(doc! {
+                    "_id": 1,
+                    "type": 1,
+                    "name": 1,
+                    "last_message": 1,
+                    "recipients": 1,
+                    "guild": 1,
+                    "owner": 1,
+                })
+                .build(),
+        ) {
+            Ok(result) => match result {
+                Some(doc) => {
+                    Some(from_bson(bson::Bson::Document(doc)).expect("Failed to unwrap channel."))
+                }
+                None => None,
+            },
+            Err(_) => None,
+        }
+    }
+
     pub fn fetch_data(&self, projection: Document) -> Option<Document> {
         database::get_collection("channels")
             .find_one(
@@ -39,25 +65,8 @@ impl<'r> FromParam<'r> for ChannelRef {
     type Error = &'r RawStr;
 
     fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
-        let id = param.to_string();
-        let result = database::get_collection("channels")
-            .find_one(
-                doc! { "_id": id },
-                FindOneOptions::builder()
-                    .projection(doc! {
-                        "_id": 1,
-                        "type": 1,
-                        "last_message": 1,
-                        "recipients": 1,
-                        "guild": 1,
-                        "owner": 1,
-                    })
-                    .build(),
-            )
-            .unwrap();
-
-        if let Some(channel) = result {
-            Ok(from_bson(bson::Bson::Document(channel)).expect("Failed to deserialize channel."))
+        if let Some(channel) = ChannelRef::from(param.to_string()) {
+            Ok(channel)
         } else {
             Err(param)
         }
diff --git a/src/guards/guild.rs b/src/guards/guild.rs
index cf23d509fa23635edf112ff70ecb2c18e1dc273b..9ca04cf27bf0e3312fd2c7223162fb2e597c9dfe 100644
--- a/src/guards/guild.rs
+++ b/src/guards/guild.rs
@@ -5,7 +5,7 @@ use rocket::request::FromParam;
 use serde::{Deserialize, Serialize};
 
 use crate::database;
-use crate::database::guild::{Ban, Member};
+use crate::database::guild::{Ban, Invite, Member};
 
 #[derive(Serialize, Deserialize, Debug, Clone)]
 pub struct GuildRef {
@@ -76,10 +76,10 @@ impl<'r> FromParam<'r> for GuildRef {
     }
 }
 
-pub fn get_member(guild: &GuildRef, member: &String) -> Option<Member> {
+pub fn get_member(guild_id: &String, member: &String) -> Option<Member> {
     if let Ok(result) = database::get_collection("members").find_one(
         doc! {
-            "_id.guild": &guild.id,
+            "_id.guild": &guild_id,
             "_id.user": &member,
         },
         None,
@@ -93,3 +93,43 @@ pub fn get_member(guild: &GuildRef, member: &String) -> Option<Member> {
         None
     }
 }
+
+pub fn get_invite(code: &String) -> Option<(String, String, Invite)> {
+    if let Ok(result) = database::get_collection("guilds").find_one(
+        doc! {
+            "invites": {
+                "$elemMatch": {
+                    "code": &code
+                }
+            }
+        },
+        FindOneOptions::builder()
+            .projection(doc! {
+                "_id": 1,
+                "name": 1,
+                "invites.$": 1,
+            })
+            .build(),
+    ) {
+        if let Some(doc) = result {
+            let invite = doc
+                .get_array("invites")
+                .unwrap()
+                .iter()
+                .next()
+                .unwrap()
+                .as_document()
+                .unwrap();
+
+            Some((
+                doc.get_str("_id").unwrap().to_string(),
+                doc.get_str("name").unwrap().to_string(),
+                from_bson(Bson::Document(invite.clone())).unwrap(),
+            ))
+        } else {
+            None
+        }
+    } else {
+        None
+    }
+}
diff --git a/src/routes/account.rs b/src/routes/account.rs
index eb854d697acc9ee5367a66c91113afdd64587c3e..7ea46100652818d93835db54ade431767f83a6d0 100644
--- a/src/routes/account.rs
+++ b/src/routes/account.rs
@@ -1,24 +1,17 @@
 use super::Response;
 use crate::database;
 use crate::email;
+use crate::util::gen_token;
 
 use bcrypt::{hash, verify};
 use bson::{doc, from_bson, Bson::UtcDatetime};
 use chrono::prelude::*;
 use database::user::User;
-use rand::{distributions::Alphanumeric, Rng};
 use rocket_contrib::json::Json;
 use serde::{Deserialize, Serialize};
 use ulid::Ulid;
 use validator::validate_email;
 
-fn gen_token(l: usize) -> String {
-    rand::thread_rng()
-        .sample_iter(&Alphanumeric)
-        .take(l)
-        .collect::<String>()
-}
-
 #[derive(Serialize, Deserialize)]
 pub struct Create {
     username: String,
diff --git a/src/routes/guild.rs b/src/routes/guild.rs
index 5fbb9fbb31fd16c3f17eced1016cca7f9d120809..1e0e74ba0f16c1d0df03c0972b54a1b41dda407a 100644
--- a/src/routes/guild.rs
+++ b/src/routes/guild.rs
@@ -2,10 +2,12 @@ use super::channel::ChannelType;
 use super::Response;
 use crate::database::{self, channel::Channel, Permission, PermissionCalculator};
 use crate::guards::auth::UserRef;
-use crate::guards::guild::{get_member, GuildRef};
+use crate::guards::channel::ChannelRef;
+use crate::guards::guild::{get_invite, get_member, GuildRef};
+use crate::util::gen_token;
 
 use bson::{doc, from_bson, Bson};
-use mongodb::options::FindOptions;
+use mongodb::options::{FindOneOptions, FindOptions};
 use rocket::request::Form;
 use rocket_contrib::json::Json;
 use serde::{Deserialize, Serialize};
@@ -194,6 +196,182 @@ pub fn create_channel(
     }
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct InviteOptions {
+    // ? TODO: add options
+}
+
+/// create a new invite
+#[post("/<target>/channels/<channel>/invite", data = "<_options>")]
+pub fn create_invite(
+    user: UserRef,
+    target: GuildRef,
+    channel: ChannelRef,
+    _options: Json<InviteOptions>,
+) -> Option<Response> {
+    let (permissions, _) = with_permissions!(user, target);
+
+    if !permissions.get_create_invite() {
+        return Some(Response::LackingPermission(Permission::CreateInvite));
+    }
+
+    let code = gen_token(7);
+    if database::get_collection("guilds")
+        .update_one(
+            doc! { "_id": target.id },
+            doc! {
+                "$push": {
+                    "invites": {
+                        "code": &code,
+                        "creator": user.id,
+                        "channel": channel.id,
+                    }
+                }
+            },
+            None,
+        )
+        .is_ok()
+    {
+        Some(Response::Success(json!({ "code": code })))
+    } else {
+        Some(Response::BadRequest(
+            json!({ "error": "Failed to create invite." }),
+        ))
+    }
+}
+
+/// remove an invite
+#[delete("/<target>/invites/<code>")]
+pub fn remove_invite(user: UserRef, target: GuildRef, code: String) -> Option<Response> {
+    let (permissions, _) = with_permissions!(user, target);
+
+    if let Some((guild_id, _, invite)) = get_invite(&code) {
+        if invite.creator != user.id {
+            if !permissions.get_manage_server() {
+                return Some(Response::LackingPermission(Permission::ManageServer));
+            }
+        }
+
+        if database::get_collection("guilds")
+            .update_one(
+                doc! {
+                    "_id": &guild_id,
+                },
+                doc! {
+                    "$pull": {
+                        "invites": {
+                            "code": &code
+                        }
+                    }
+                },
+                None,
+            )
+            .is_ok()
+        {
+            Some(Response::Result(super::Status::Ok))
+        } else {
+            Some(Response::BadRequest(
+                json!({ "error": "Failed to delete invite." }),
+            ))
+        }
+    } else {
+        Some(Response::NotFound(
+            json!({ "error": "Failed to fetch invite or code is invalid." }),
+        ))
+    }
+}
+
+/// fetch all guild invites
+#[get("/<target>/invites")]
+pub fn fetch_invites(user: UserRef, target: GuildRef) -> Option<Response> {
+    let (permissions, _) = with_permissions!(user, target);
+
+    if !permissions.get_manage_server() {
+        return Some(Response::LackingPermission(Permission::ManageServer));
+    }
+
+    if let Some(doc) = target.fetch_data(doc! {
+        "invites": 1,
+    }) {
+        Some(Response::Success(json!(doc.get_array("invites").unwrap())))
+    } else {
+        Some(Response::InternalServerError(
+            json!({ "error": "Failed to fetch invites." }),
+        ))
+    }
+}
+
+/// view an invite before joining
+#[get("/join/<code>", rank = 1)]
+pub fn fetch_invite(_user: UserRef, code: String) -> Response {
+    if let Some((guild_id, name, invite)) = get_invite(&code) {
+        if let Some(channel) = ChannelRef::from(invite.channel) {
+            Response::Success(json!({
+                "guild": {
+                    "id": guild_id,
+                    "name": name,
+                },
+                "channel": {
+                    "id": channel.id,
+                    "name": channel.name,
+                }
+            }))
+        } else {
+            Response::BadRequest(json!({ "error": "Failed to fetch channel." }))
+        }
+    } else {
+        Response::NotFound(json!({ "error": "Failed to fetch invite or code is invalid." }))
+    }
+}
+
+/// join a guild using an invite
+#[post("/join/<code>", rank = 1)]
+pub fn use_invite(user: UserRef, code: String) -> Response {
+    if let Some((guild_id, _, invite)) = get_invite(&code) {
+        if let Ok(result) = database::get_collection("members").find_one(
+            doc! {
+                "_id.guild": &guild_id,
+                "_id.user": &user.id
+            },
+            FindOneOptions::builder()
+                .projection(doc! { "_id": 1 })
+                .build(),
+        ) {
+            if result.is_none() {
+                if database::get_collection("members")
+                    .insert_one(
+                        doc! {
+                            "_id": {
+                                "guild": &guild_id,
+                                "user": &user.id
+                            }
+                        },
+                        None,
+                    )
+                    .is_ok()
+                {
+                    Response::Success(json!({
+                        "guild": &guild_id,
+                        "channel": &invite.channel,
+                    }))
+                } else {
+                    Response::InternalServerError(
+                        json!({ "error": "Failed to add you to the guild." }),
+                    )
+                }
+            } else {
+                Response::BadRequest(json!({ "error": "Already in the guild." }))
+            }
+        } else {
+            Response::InternalServerError(
+                json!({ "error": "Failed to check if you're in the guild." }),
+            )
+        }
+    } else {
+        Response::NotFound(json!({ "error": "Failed to fetch invite or code is invalid." }))
+    }
+}
+
 #[derive(Serialize, Deserialize)]
 pub struct CreateGuild {
     name: String,
@@ -323,7 +501,7 @@ pub fn fetch_members(user: UserRef, target: GuildRef) -> Option<Response> {
 pub fn fetch_member(user: UserRef, target: GuildRef, other: String) -> Option<Response> {
     with_permissions!(user, target);
 
-    if let Some(member) = get_member(&target, &other) {
+    if let Some(member) = get_member(&target.id, &other) {
         Some(Response::Success(json!({
             "id": member.id.user,
             "nickname": member.nickname,
@@ -350,7 +528,7 @@ pub fn kick_member(user: UserRef, target: GuildRef, other: String) -> Option<Res
         return Some(Response::LackingPermission(Permission::KickMembers));
     }
 
-    if get_member(&target, &other).is_none() {
+    if get_member(&target.id, &other).is_none() {
         return Some(Response::BadRequest(
             json!({ "error": "User not part of guild." }),
         ));
@@ -406,7 +584,7 @@ pub fn ban_member(
         return Some(Response::LackingPermission(Permission::BanMembers));
     }
 
-    if get_member(&target, &other).is_none() {
+    if get_member(&target.id, &other).is_none() {
         return Some(Response::BadRequest(
             json!({ "error": "User not part of guild." }),
         ));
diff --git a/src/routes/mod.rs b/src/routes/mod.rs
index 6e68b4128751fd1e40ba2415928c8b8168cf3de1..c847d799a5e355d256021dbf79f7b4e9c05f2b3e 100644
--- a/src/routes/mod.rs
+++ b/src/routes/mod.rs
@@ -107,6 +107,11 @@ pub fn mount(rocket: Rocket) -> Rocket {
                 guild::my_guilds,
                 guild::guild,
                 guild::create_channel,
+                guild::create_invite,
+                guild::remove_invite,
+                guild::fetch_invites,
+                guild::fetch_invite,
+                guild::use_invite,
                 guild::create_guild,
                 guild::fetch_members,
                 guild::fetch_member,
diff --git a/src/util/mod.rs b/src/util/mod.rs
index c2f6c42c532cf4297ad259dabc387cb8c6ae671a..038b896d720124043efcec3763eafd2633297bd1 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -1,6 +1,14 @@
 use hashbrown::HashSet;
+use rand::{distributions::Alphanumeric, Rng};
 use std::iter::FromIterator;
 
 pub fn vec_to_set<T: Clone + Eq + std::hash::Hash>(data: &[T]) -> HashSet<T> {
     HashSet::from_iter(data.iter().cloned())
 }
+
+pub fn gen_token(l: usize) -> String {
+    rand::thread_rng()
+        .sample_iter(&Alphanumeric)
+        .take(l)
+        .collect::<String>()
+}