diff --git a/api.md b/api.md
new file mode 100644
index 0000000..1680fb6
--- /dev/null
+++ b/api.md
@@ -0,0 +1,946 @@
+
+
+
Personal Web Services API v1.0.0
+
+> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu.
+
+Documentation for various web services hosted under mono.exhq.dev
+
+Discord Lookup
+
+Discord user information lookup service
+
+## get__v1_user_{userId}
+
+> Code samples
+
+```shell
+# You can also use wget
+curl -X GET /v1/user/{userId} \
+ -H 'Accept: application/json'
+
+```
+
+```http
+GET /v1/user/{userId} HTTP/1.1
+
+Accept: application/json
+
+```
+
+```javascript
+
+const headers = {
+ 'Accept':'application/json'
+};
+
+fetch('/v1/user/{userId}',
+{
+ method: 'GET',
+
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+```ruby
+require 'rest-client'
+require 'json'
+
+headers = {
+ 'Accept' => 'application/json'
+}
+
+result = RestClient.get '/v1/user/{userId}',
+ params: {
+ }, headers: headers
+
+p JSON.parse(result)
+
+```
+
+```python
+import requests
+headers = {
+ 'Accept': 'application/json'
+}
+
+r = requests.get('/v1/user/{userId}', headers = headers)
+
+print(r.json())
+
+```
+
+```php
+ 'application/json',
+);
+
+$client = new \GuzzleHttp\Client();
+
+// Define array of request body.
+$request_body = array();
+
+try {
+ $response = $client->request('GET','/v1/user/{userId}', array(
+ 'headers' => $headers,
+ 'json' => $request_body,
+ )
+ );
+ print_r($response->getBody()->getContents());
+ }
+ catch (\GuzzleHttp\Exception\BadResponseException $e) {
+ // handle exception or api errors.
+ print_r($e->getMessage());
+ }
+
+ // ...
+
+```
+
+```java
+URL obj = new URL("/v1/user/{userId}");
+HttpURLConnection con = (HttpURLConnection) obj.openConnection();
+con.setRequestMethod("GET");
+int responseCode = con.getResponseCode();
+BufferedReader in = new BufferedReader(
+ new InputStreamReader(con.getInputStream()));
+String inputLine;
+StringBuffer response = new StringBuffer();
+while ((inputLine = in.readLine()) != null) {
+ response.append(inputLine);
+}
+in.close();
+System.out.println(response.toString());
+
+```
+
+```go
+package main
+
+import (
+ "bytes"
+ "net/http"
+)
+
+func main() {
+
+ headers := map[string][]string{
+ "Accept": []string{"application/json"},
+ }
+
+ data := bytes.NewBuffer([]byte{jsonReq})
+ req, err := http.NewRequest("GET", "/v1/user/{userId}", data)
+ req.Header = headers
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ // ...
+}
+
+```
+
+`GET /v1/user/{userId}`
+
+*Look up Discord user information*
+
+Retrieves information about a Discord user using their ID
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|userId|path|string|true|none|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "id": "712639419785412668",
+ "created_at": "2020-05-20T12:14:37.877Z",
+ "username": "exhq",
+ "avatar": {
+ "id": "93dd1fc1629fb1ec652c68203af4f3f1",
+ "link": "https://cdn.discordapp.com/avatars/712639419785412668/93dd1fc1629fb1ec652c68203af4f3f1",
+ "is_animated": false
+ },
+ "avatar_decoration": null,
+ "badges": [
+ "HOUSE_BALANCE"
+ ],
+ "accent_color": null,
+ "global_name": "ECHO 🐈",
+ "banner": {
+ "id": null,
+ "link": null,
+ "is_animated": false,
+ "color": null
+ },
+ "raw": {
+ "id": "712639419785412668",
+ "username": "exhq",
+ "avatar": "93dd1fc1629fb1ec652c68203af4f3f1",
+ "discriminator": "0",
+ "public_flags": 256,
+ "flags": 256,
+ "banner": null,
+ "accent_color": null,
+ "global_name": "ECHO 🐈",
+ "avatar_decoration_data": null,
+ "banner_color": null,
+ "clan": null
+ }
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful response with user information|[DiscordUser](#schemadiscorduser)|
+
+
+This operation does not require authentication
+
+
+Review System
+
+Backend review system for website
+
+## get__getreviews
+
+> Code samples
+
+```shell
+# You can also use wget
+curl -X GET /getreviews \
+ -H 'Accept: application/json'
+
+```
+
+```http
+GET /getreviews HTTP/1.1
+
+Accept: application/json
+
+```
+
+```javascript
+
+const headers = {
+ 'Accept':'application/json'
+};
+
+fetch('/getreviews',
+{
+ method: 'GET',
+
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+```ruby
+require 'rest-client'
+require 'json'
+
+headers = {
+ 'Accept' => 'application/json'
+}
+
+result = RestClient.get '/getreviews',
+ params: {
+ }, headers: headers
+
+p JSON.parse(result)
+
+```
+
+```python
+import requests
+headers = {
+ 'Accept': 'application/json'
+}
+
+r = requests.get('/getreviews', headers = headers)
+
+print(r.json())
+
+```
+
+```php
+ 'application/json',
+);
+
+$client = new \GuzzleHttp\Client();
+
+// Define array of request body.
+$request_body = array();
+
+try {
+ $response = $client->request('GET','/getreviews', array(
+ 'headers' => $headers,
+ 'json' => $request_body,
+ )
+ );
+ print_r($response->getBody()->getContents());
+ }
+ catch (\GuzzleHttp\Exception\BadResponseException $e) {
+ // handle exception or api errors.
+ print_r($e->getMessage());
+ }
+
+ // ...
+
+```
+
+```java
+URL obj = new URL("/getreviews");
+HttpURLConnection con = (HttpURLConnection) obj.openConnection();
+con.setRequestMethod("GET");
+int responseCode = con.getResponseCode();
+BufferedReader in = new BufferedReader(
+ new InputStreamReader(con.getInputStream()));
+String inputLine;
+StringBuffer response = new StringBuffer();
+while ((inputLine = in.readLine()) != null) {
+ response.append(inputLine);
+}
+in.close();
+System.out.println(response.toString());
+
+```
+
+```go
+package main
+
+import (
+ "bytes"
+ "net/http"
+)
+
+func main() {
+
+ headers := map[string][]string{
+ "Accept": []string{"application/json"},
+ }
+
+ data := bytes.NewBuffer([]byte{jsonReq})
+ req, err := http.NewRequest("GET", "/getreviews", data)
+ req.Header = headers
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ // ...
+}
+
+```
+
+`GET /getreviews`
+
+*Get reviews*
+
+Retrieves reviews from the backend review system
+
+> Example responses
+
+> 200 Response
+
+```json
+[
+ {
+ "reviewID": 1,
+ "discordID": "712653921692155965",
+ "reviewText": "meow",
+ "timestamp": "1708902649485"
+ }
+]
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successful response with reviews|Inline|
+
+Response Schema
+
+Status Code **200**
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|*anonymous*|[[Review](#schemareview)]|false|none|none|
+|» reviewID|integer|false|none|none|
+|» discordID|string|false|none|none|
+|» reviewText|string|false|none|none|
+|» timestamp|string|false|none|none|
+
+
+This operation does not require authentication
+
+
+Song Link Cache
+
+Cached proxy for song.link API
+
+## get__
+
+> Code samples
+
+```shell
+# You can also use wget
+curl -X GET /?url=spotify%3Atrack%3A6BJHsLiE47Sk0wQkuppqhr \
+ -H 'Accept: application/json'
+
+```
+
+```http
+GET /?url=spotify%3Atrack%3A6BJHsLiE47Sk0wQkuppqhr HTTP/1.1
+
+Accept: application/json
+
+```
+
+```javascript
+
+const headers = {
+ 'Accept':'application/json'
+};
+
+fetch('/?url=spotify%3Atrack%3A6BJHsLiE47Sk0wQkuppqhr',
+{
+ method: 'GET',
+
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+```ruby
+require 'rest-client'
+require 'json'
+
+headers = {
+ 'Accept' => 'application/json'
+}
+
+result = RestClient.get '/',
+ params: {
+ 'url' => 'string'
+}, headers: headers
+
+p JSON.parse(result)
+
+```
+
+```python
+import requests
+headers = {
+ 'Accept': 'application/json'
+}
+
+r = requests.get('/', params={
+ 'url': 'spotify:track:6BJHsLiE47Sk0wQkuppqhr'
+}, headers = headers)
+
+print(r.json())
+
+```
+
+```php
+ 'application/json',
+);
+
+$client = new \GuzzleHttp\Client();
+
+// Define array of request body.
+$request_body = array();
+
+try {
+ $response = $client->request('GET','/', array(
+ 'headers' => $headers,
+ 'json' => $request_body,
+ )
+ );
+ print_r($response->getBody()->getContents());
+ }
+ catch (\GuzzleHttp\Exception\BadResponseException $e) {
+ // handle exception or api errors.
+ print_r($e->getMessage());
+ }
+
+ // ...
+
+```
+
+```java
+URL obj = new URL("/?url=spotify%3Atrack%3A6BJHsLiE47Sk0wQkuppqhr");
+HttpURLConnection con = (HttpURLConnection) obj.openConnection();
+con.setRequestMethod("GET");
+int responseCode = con.getResponseCode();
+BufferedReader in = new BufferedReader(
+ new InputStreamReader(con.getInputStream()));
+String inputLine;
+StringBuffer response = new StringBuffer();
+while ((inputLine = in.readLine()) != null) {
+ response.append(inputLine);
+}
+in.close();
+System.out.println(response.toString());
+
+```
+
+```go
+package main
+
+import (
+ "bytes"
+ "net/http"
+)
+
+func main() {
+
+ headers := map[string][]string{
+ "Accept": []string{"application/json"},
+ }
+
+ data := bytes.NewBuffer([]byte{jsonReq})
+ req, err := http.NewRequest("GET", "/", data)
+ req.Header = headers
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ // ...
+}
+
+```
+
+`GET /`
+
+*Get cached song.link data*
+
+Cached proxy for song.link API responses with CORS support
+
+Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|url|query|string|true|Encoded song URL or identifier|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "entityUniqueId": "SPOTIFY_SONG::6BJHsLiE47Sk0wQkuppqhr",
+ "userCountry": "US",
+ "pageUrl": "https://song.link/s/6BJHsLiE47Sk0wQkuppqhr",
+ "entitiesByUniqueId": {
+ "property1": {
+ "id": "6BJHsLiE47Sk0wQkuppqhr",
+ "type": "song",
+ "title": "From the Start",
+ "artistName": "Good Kid",
+ "thumbnailUrl": "https://i.scdn.co/image/ab67616d0000b273e67835d0a5d81fa4f268b513",
+ "thumbnailWidth": 640,
+ "thumbnailHeight": 640,
+ "apiProvider": "spotify",
+ "platforms": [
+ "spotify"
+ ]
+ },
+ "property2": {
+ "id": "6BJHsLiE47Sk0wQkuppqhr",
+ "type": "song",
+ "title": "From the Start",
+ "artistName": "Good Kid",
+ "thumbnailUrl": "https://i.scdn.co/image/ab67616d0000b273e67835d0a5d81fa4f268b513",
+ "thumbnailWidth": 640,
+ "thumbnailHeight": 640,
+ "apiProvider": "spotify",
+ "platforms": [
+ "spotify"
+ ]
+ }
+ },
+ "linksByPlatform": {
+ "property1": {
+ "country": "US",
+ "url": "https://open.spotify.com/track/6BJHsLiE47Sk0wQkuppqhr",
+ "entityUniqueId": "SPOTIFY_SONG::6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriMobile": "spotify:track:6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriDesktop": "spotify:track:6BJHsLiE47Sk0wQkuppqhr"
+ },
+ "property2": {
+ "country": "US",
+ "url": "https://open.spotify.com/track/6BJHsLiE47Sk0wQkuppqhr",
+ "entityUniqueId": "SPOTIFY_SONG::6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriMobile": "spotify:track:6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriDesktop": "spotify:track:6BJHsLiE47Sk0wQkuppqhr"
+ }
+ }
+}
+```
+
+Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Cached song.link response|[SongLinkResponse](#schemasonglinkresponse)|
+
+
+This operation does not require authentication
+
+
+# Schemas
+
+DiscordUser
+
+
+
+
+
+
+```json
+{
+ "id": "712639419785412668",
+ "created_at": "2020-05-20T12:14:37.877Z",
+ "username": "exhq",
+ "avatar": {
+ "id": "93dd1fc1629fb1ec652c68203af4f3f1",
+ "link": "https://cdn.discordapp.com/avatars/712639419785412668/93dd1fc1629fb1ec652c68203af4f3f1",
+ "is_animated": false
+ },
+ "avatar_decoration": null,
+ "badges": [
+ "HOUSE_BALANCE"
+ ],
+ "accent_color": null,
+ "global_name": "ECHO 🐈",
+ "banner": {
+ "id": null,
+ "link": null,
+ "is_animated": false,
+ "color": null
+ },
+ "raw": {
+ "id": "712639419785412668",
+ "username": "exhq",
+ "avatar": "93dd1fc1629fb1ec652c68203af4f3f1",
+ "discriminator": "0",
+ "public_flags": 256,
+ "flags": 256,
+ "banner": null,
+ "accent_color": null,
+ "global_name": "ECHO 🐈",
+ "avatar_decoration_data": null,
+ "banner_color": null,
+ "clan": null
+ }
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|none|
+|created_at|string(date-time)|false|none|none|
+|username|string|false|none|none|
+|avatar|[DiscordAvatar](#schemadiscordavatar)|false|none|none|
+|avatar_decoration|string¦null|false|none|none|
+|badges|[string]|false|none|none|
+|accent_color|string¦null|false|none|none|
+|global_name|string|false|none|none|
+|banner|[DiscordBanner](#schemadiscordbanner)|false|none|none|
+|raw|[DiscordRawData](#schemadiscordrawdata)|false|none|none|
+
+DiscordAvatar
+
+
+
+
+
+
+```json
+{
+ "id": "93dd1fc1629fb1ec652c68203af4f3f1",
+ "link": "https://cdn.discordapp.com/avatars/712639419785412668/93dd1fc1629fb1ec652c68203af4f3f1",
+ "is_animated": false
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|none|
+|link|string|false|none|none|
+|is_animated|boolean|false|none|none|
+
+DiscordBanner
+
+
+
+
+
+
+```json
+{
+ "id": null,
+ "link": null,
+ "is_animated": false,
+ "color": null
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string¦null|false|none|none|
+|link|string¦null|false|none|none|
+|is_animated|boolean|false|none|none|
+|color|string¦null|false|none|none|
+
+DiscordRawData
+
+
+
+
+
+
+```json
+{
+ "id": "712639419785412668",
+ "username": "exhq",
+ "avatar": "93dd1fc1629fb1ec652c68203af4f3f1",
+ "discriminator": "0",
+ "public_flags": 256,
+ "flags": 256,
+ "banner": null,
+ "accent_color": null,
+ "global_name": "ECHO 🐈",
+ "avatar_decoration_data": null,
+ "banner_color": null,
+ "clan": null
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|none|
+|username|string|false|none|none|
+|avatar|string|false|none|none|
+|discriminator|string|false|none|none|
+|public_flags|integer|false|none|none|
+|flags|integer|false|none|none|
+|banner|string¦null|false|none|none|
+|accent_color|string¦null|false|none|none|
+|global_name|string|false|none|none|
+|avatar_decoration_data|string¦null|false|none|none|
+|banner_color|string¦null|false|none|none|
+|clan|string¦null|false|none|none|
+
+Review
+
+
+
+
+
+
+```json
+{
+ "reviewID": 1,
+ "discordID": "712653921692155965",
+ "reviewText": "meow",
+ "timestamp": "1708902649485"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|reviewID|integer|false|none|none|
+|discordID|string|false|none|none|
+|reviewText|string|false|none|none|
+|timestamp|string|false|none|none|
+
+SongLinkResponse
+
+
+
+
+
+
+```json
+{
+ "entityUniqueId": "SPOTIFY_SONG::6BJHsLiE47Sk0wQkuppqhr",
+ "userCountry": "US",
+ "pageUrl": "https://song.link/s/6BJHsLiE47Sk0wQkuppqhr",
+ "entitiesByUniqueId": {
+ "property1": {
+ "id": "6BJHsLiE47Sk0wQkuppqhr",
+ "type": "song",
+ "title": "From the Start",
+ "artistName": "Good Kid",
+ "thumbnailUrl": "https://i.scdn.co/image/ab67616d0000b273e67835d0a5d81fa4f268b513",
+ "thumbnailWidth": 640,
+ "thumbnailHeight": 640,
+ "apiProvider": "spotify",
+ "platforms": [
+ "spotify"
+ ]
+ },
+ "property2": {
+ "id": "6BJHsLiE47Sk0wQkuppqhr",
+ "type": "song",
+ "title": "From the Start",
+ "artistName": "Good Kid",
+ "thumbnailUrl": "https://i.scdn.co/image/ab67616d0000b273e67835d0a5d81fa4f268b513",
+ "thumbnailWidth": 640,
+ "thumbnailHeight": 640,
+ "apiProvider": "spotify",
+ "platforms": [
+ "spotify"
+ ]
+ }
+ },
+ "linksByPlatform": {
+ "property1": {
+ "country": "US",
+ "url": "https://open.spotify.com/track/6BJHsLiE47Sk0wQkuppqhr",
+ "entityUniqueId": "SPOTIFY_SONG::6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriMobile": "spotify:track:6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriDesktop": "spotify:track:6BJHsLiE47Sk0wQkuppqhr"
+ },
+ "property2": {
+ "country": "US",
+ "url": "https://open.spotify.com/track/6BJHsLiE47Sk0wQkuppqhr",
+ "entityUniqueId": "SPOTIFY_SONG::6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriMobile": "spotify:track:6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriDesktop": "spotify:track:6BJHsLiE47Sk0wQkuppqhr"
+ }
+ }
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|entityUniqueId|string|false|none|none|
+|userCountry|string|false|none|none|
+|pageUrl|string|false|none|none|
+|entitiesByUniqueId|object|false|none|none|
+|» **additionalProperties**|[SongEntity](#schemasongentity)|false|none|none|
+|linksByPlatform|object|false|none|none|
+|» **additionalProperties**|[PlatformLink](#schemaplatformlink)|false|none|none|
+
+SongEntity
+
+
+
+
+
+
+```json
+{
+ "id": "6BJHsLiE47Sk0wQkuppqhr",
+ "type": "song",
+ "title": "From the Start",
+ "artistName": "Good Kid",
+ "thumbnailUrl": "https://i.scdn.co/image/ab67616d0000b273e67835d0a5d81fa4f268b513",
+ "thumbnailWidth": 640,
+ "thumbnailHeight": 640,
+ "apiProvider": "spotify",
+ "platforms": [
+ "spotify"
+ ]
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|id|string|false|none|none|
+|type|string|false|none|none|
+|title|string|false|none|none|
+|artistName|string|false|none|none|
+|thumbnailUrl|string|false|none|none|
+|thumbnailWidth|integer|false|none|none|
+|thumbnailHeight|integer|false|none|none|
+|apiProvider|string|false|none|none|
+|platforms|[string]|false|none|none|
+
+
+
+
+
+
+
+
+```json
+{
+ "country": "US",
+ "url": "https://open.spotify.com/track/6BJHsLiE47Sk0wQkuppqhr",
+ "entityUniqueId": "SPOTIFY_SONG::6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriMobile": "spotify:track:6BJHsLiE47Sk0wQkuppqhr",
+ "nativeAppUriDesktop": "spotify:track:6BJHsLiE47Sk0wQkuppqhr"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|country|string|false|none|none|
+|url|string|false|none|none|
+|entityUniqueId|string|false|none|none|
+|nativeAppUriMobile|string|false|none|none|
+|nativeAppUriDesktop|string|false|none|none|
+