diff --git a/src/api.ts b/src/api.ts
index 55d6437..d421bab 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -3,8 +3,8 @@ import * as base64id from "base64id";
 import { config } from "./config";
 import {unlink} from "fs";
 
-async function register(name: string, password: string, confirm: string): Promise<object> {
-	if(!config['satyr']['registration']) return {"error":"registration disabled"};
+async function register(name: string, password: string, confirm: string, invite?: boolean): Promise<object> {
+	if(!config['satyr']['registration'] && !invite) return {"error":"registration disabled"};
 	if(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"};
 	if(password !== confirm) return {"error":"mismatched passwords"};
 	for(let i=0;i<config['satyr']['restrictedNames'].length;i++){
@@ -104,12 +104,15 @@ async function genInvite(): Promise<string>{
 	return invitecode;
 }
 
-async function useInvite(code: string): Promise<boolean>{
+async function validInvite(code: string): Promise<boolean>{
 	if(typeof(code) !== "string" || code === "") return false;
 	var result = await db.query('SELECT code FROM invites WHERE code='+db.raw.escape(code));
 	if(!result[0] || result[0]['code'] !== code) return false;
-	await db.query('DELETE FROM invites WHERE code='+db.raw.escape(code));
 	return true;
 }
 
-export { register, update, changepwd, changesk, login, updateChat, deleteVODs, getConfig, genInvite, useInvite };
\ No newline at end of file
+async function useInvite(code: string): Promise<void>{
+	if(validInvite(code)) await db.query('DELETE FROM invites WHERE code='+db.raw.escape(code));
+}
+
+export { register, update, changepwd, changesk, login, updateChat, deleteVODs, getConfig, genInvite, useInvite, validInvite };
\ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
index 4bd6ec0..0fdeaa4 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -16,7 +16,7 @@ const config: Object = {
 	   domain: '',
 	   registration: false,
 	   email: null,
-	   restrictedNames: [ 'live', 'user', 'users', 'register', 'login' ],
+	   restrictedNames: [ 'live', 'user', 'users', 'register', 'login', 'invite' ],
 	   rootredirect: '/users/live',
 	   version: process.env.npm_package_version,
 	 }, localconfig['satyr']),
diff --git a/src/http.ts b/src/http.ts
index b5f9c53..4f2544d 100644
--- a/src/http.ts
+++ b/src/http.ts
@@ -224,6 +224,21 @@ async function initAPI() {
 		});
 	});
 	app.post('/api/register', (req, res) => {
+		if("invite" in req.body){
+			if(api.validInvite(req.body.invite)){
+				api.register(req.body.username, req.body.password, req.body.confirm, true).then((result) => {
+					if(result[0]) return genToken(req.body.username).then((t) => {
+						res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
+						res.json(result);
+						api.useInvite(req.body.invite);
+						return;
+					});
+					res.json(result);
+				});
+			}
+			else res.json({error: "invalid invite code"});
+		}
+		else
 		api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => {
 			if(result[0]) return genToken(req.body.username).then((t) => {
 				res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
@@ -486,6 +501,18 @@ async function initSite(openReg) {
 		}
 		else res.render('login.njk',njkconf);
 	});
+	app.get('/invite/:code', (req, res) => {
+		if(tryDecode(req.cookies.Authorization)) {
+			res.redirect('/profile');
+		}
+		else res.render('invite.njk',Object.assign({icode: req.params.code}, njkconf));
+	});
+	app.get('/invite', (req, res) => {
+		if(tryDecode(req.cookies.Authorization)) {
+			res.redirect('/profile');
+		}
+		else res.render('invite.njk',Object.assign({icode: ""}, njkconf));
+	});
 	app.get('/register', (req, res) => {
 		if(tryDecode(req.cookies.Authorization) || !openReg) {
 			res.redirect(njkconf.rootredirect);
diff --git a/templates/invite.njk b/templates/invite.njk
new file mode 100644
index 0000000..9b2b30b
--- /dev/null
+++ b/templates/invite.njk
@@ -0,0 +1,20 @@
+{% extends "base.njk" %}
+{% block content %}
+<h3>You've been invited to {{ sitename }}</h3><span style="font-size: small;">Already registered? Log in <a href="/login">here</a>.</br></br></span>
+	<!--<div id="jscontainer" style="height: 100%;">
+		<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">-->
+			<form action="/api/register" method="POST" target="responseFrame">
+				Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
+        		Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
+				Confirm: </br><input type="password" name="confirm" style="min-width: 300px"/></br>
+				Invite Code: </br><input type="text" name="invite" style="min-width: 300px" value="{{icode}}"/></br></br>
+				<input type="submit" value="Submit">
+			</form></br>
+			
+		<!--</div>
+		<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">-->
+			{% include "tos.html" %}</br>
+			<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
+		<!--</div>
+	</div>-->
+{% endblock %}
\ No newline at end of file