# Draft version. Refer to the official API documentation for more accurate information. openapi: 3.0.0 info: title: PluralKit version: "1.1" description: | This is the API for [PluralKit](https://pluralkit.me/)! :) The API itself is stable, but this document (the OpenAPI description) is still subject to change, and may be updated, corrected or restructured in the future (as long as it's still coherent with the real API). # Authentication Authentication is handled using a "system token". At the moment, the only way to obtain a system token is to use the `pk;token` command through the Discord bot. This will generate an opaque string you must pass as the `Authorization` header to API requests. Many API endpoints are available anonymously, but most of them will hide information from unauthenticated requests to align with the relevant privacy settings. # Errors Errors are just returned as HTTP response codes. Most error responses include a human-readable error message as the body, but this should not be relied on. Just read the response codes :) # OpenAPI version history - **1.1**: Granular member privacy - **1.0**: (initial definition version) license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html externalDocs: url: https://pluralkit.me/api description: For more information, see the official PluralKit API documentation on the website. servers: - url: https://api.pluralkit.me/v1 description: Primary API server (v1) paths: /s: get: summary: Returns your own system. description: Requires authentication, and will returns the system the token belongs to. tags: [Systems] operationId: GetOwnSystem security: - TokenAuth: [] responses: "200": $ref: "#/components/responses/SystemResponse" "401": $ref: "#/components/responses/UnauthorizedError" patch: summary: Updates an existing system. description: Requires authentication, and will update the system the token belongs to. tags: [Systems] operationId: UpdateSystem security: - TokenAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/System" responses: "200": description: The system was updated. Returns the updated system object. content: application/json: schema: $ref: "#/components/schemas/System" "401": $ref: "#/components/responses/UnauthorizedError" /s/{id}: parameters: - $ref: "#/components/parameters/SystemID" get: summary: Gets a system by its ID. tags: [Systems] operationId: GetSystem description: Partial information may be returned if not authenticated with this system's token. responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/System" "404": description: System with the given ID not found. /s/{id}/members: parameters: - $ref: "#/components/parameters/SystemID" get: summary: Gets a system's members. description: | If the API token does not belong to this system, this list may exclude any private members in the system. tags: [Systems, Members] operationId: GetSystemMembers responses: "200": description: OK content: application/json: schema: type: array items: $ref: "#/components/schemas/System" "403": description: The system's member list is private, and the given token either missing, invalid, or does not correspond to the system. "404": description: System with the given ID not found. /s/{id}/fronters: # TODO: rename to "latest switch" or something? parameters: - $ref: "#/components/parameters/SystemID" get: summary: Gets a system's current fronters. tags: [Systems, Switches] operationId: GetSystemFronters responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/FullSwitch" "403": description: The system's current fronters are private, and the given token is either missing, invalid, or does not correspond to the system. "404": description: System with the given ID was either not found, or does not have any switches registered. /s/{id}/switches: parameters: - $ref: "#/components/parameters/SystemID" get: summary: Gets a system's switch history. description: | Will return the system's switch history, up to 100 entries at a time, in reverse-chronological (latest first) order. For pagination, see the `before` query parameter. tags: [Systems, Switches] operationId: GetSystemSwitches parameters: - in: query name: before schema: type: string format: date-time description: | If provided, will only return switches that happened *before* (and not including) this timestamp. This can be used for pagination by calling the endpoint again with the timestamp of the last switch of the previous response. responses: "200": description: OK content: application/json: schema: type: array maxItems: 100 items: $ref: "#/components/schemas/Switch" "403": description: The system's switch history is private, and the given token is either missing, invalid, or does not correspond to the system. "404": description: System with the given ID not found. /s/switches: post: summary: Registers a new switch. tags: [Switches] operationId: RegisterSwitch requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Switch" security: - TokenAuth: [] responses: "204": description: The switch was logged. "401": $ref: "#/components/responses/UnauthorizedError" /m: post: summary: Creates a new member in your system. tags: [Members] operationId: CreateMember requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Member" security: - TokenAuth: [] responses: "200": description: The member was created. Returns the newly created member object. content: application/json: schema: $ref: "#/components/schemas/Member" "401": $ref: "#/components/responses/UnauthorizedError" /m/{id}: parameters: - $ref: "#/components/parameters/MemberID" get: summary: Gets a member by their ID. tags: [Members] operationId: GetMember security: - {} - TokenAuth: [] responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/Member" "404": $ref: "#/components/responses/MemberNotFoundError" patch: summary: Updates a member. tags: [Members] operationId: UpdateMember requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Member" security: - TokenAuth: [] responses: "200": description: The member was updated. Returns trhe updated member object. content: application/json: schema: $ref: "#/components/schemas/Member" "401": $ref: "#/components/responses/MemberAuthError" "404": $ref: "#/components/responses/MemberNotFoundError" delete: summary: Deletes a member. tags: [Members] operationId: DeleteMember security: - TokenAuth: [] responses: "200": # TODO: should be 204 (but *is* 200) description: Member successfully deleted. "401": $ref: "#/components/responses/MemberAuthError" "404": $ref: "#/components/responses/MemberNotFoundError" /a/{id}: parameters: - in: path name: id required: true description: A Discord user ID. schema: $ref: "#/components/schemas/Snowflake" get: summary: Gets a system by (one of) its associated Discord accounts. description: | Note that it's currently not possible to get a system's registered accounts given a system ID through the API. Consider this endpoint "one-way". tags: [Systems, Accounts] responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/System" "404": description: The given user ID does not correspond to any systems. /msg/{id}: parameters: - in: path name: id required: true description: | A Discord message ID. This may refer to either the original "trigger message" posted by the user, or to the resulting webhook message posted by PluralKit. The former may be useful for eg. logging bot integration. schema: $ref: "#/components/schemas/Snowflake" get: summary: Gets information about a proxied message by its message ID. tags: [Proxying] responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/Message" "404": description: The given message ID does not correspond to a message proxied by PluralKit. components: responses: UnauthorizedError: description: System token is missing or invalid. NotOwnSystemError: description: The given system does not correspond with this token. MemberAuthError: # TODO (relevant, ish): This is always returned as 401 but should be split into "invalid token" (401) and "not own member" (403) responses description: System token is missing, invalid, or the corresponding system does not own the given member. MemberNotFoundError: description: A member by the given ID was not found. SystemResponse: description: OK content: application/json: schema: $ref: "#/components/schemas/System" MemberListResponse: description: OK content: application/json: schema: type: array items: $ref: "#/components/schemas/Member" parameters: SystemID: in: path name: id required: true description: The ID of the system in question. schema: $ref: "#/components/schemas/ID" MemberID: in: path name: id required: true description: The ID of the member in question. schema: $ref: "#/components/schemas/ID" schemas: ID: title: ID (system or member) type: string readOnly: true minLength: 5 maxLength: 5 pattern: "^[a-z]{5}$" example: "abcde" description: | A unique identifier for a system or a member, as a randomly generated string of five lowercase letters. These IDs are guaranteed to be globally unique for the given model type (a system can have the same ID as a member, but two systems or two members can never share an ID). The IDs can on rare occasions change - eg. if a profane word is generated and later regenerated, or as a potential future Patreon perk. However, it's still reasonable to assume that the IDs are constant, and that tey won't change without the user's knowledge, so it's safe to store in things like settings menus and config files. System: properties: id: $ref: "#/components/schemas/ID" name: type: string nullable: true maxLength: 100 description: The user-provided name of the system. example: "Boxes of Foxes" description: type: string nullable: true maxLength: 1000 description: | The user-provided system description. May contain rich text in Markdown, including standard Markdown-formatted links, or Discord-formatted emoji/user/channel references. example: |- This system is very cool. It has cool people. tag: type: string maxLength: 78 description: | The system tag, which is appended to the names (or display names, if set) of members when proxying through the bot. A space will automatically be inserted between the name and the tag, so no need to include one at the start. The character limit here is 78, as Discord's name length limit for webhooks is 80 characters. A 78-character system tag is thus the longest tag that can still accomodate a single-letter member name and the separating space. nullable: true example: "| BoF" avatar_url: type: string format: url nullable: true maxLength: 256 example: "https://i.imgur.com/Abcdefg.png" description: | A link to the avatar/icon of the system. If used for proxying, the image must be at most 1000 pixels in width *and* height, and at most 1 MiB in size. This restriction is on Discord's end and is not verified through the API (it's simply stored as a string). tz: type: string format: timezone nullable: true default: UTC example: America/New_York description: | The system's registered time zone as an Olson time zone ID. This is used in the bot to convert various time stamps in commands on behalf of this system. created: type: string format: date-time readOnly: true description: The creation timestamp of the system. description_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The system's description privacy setting, either "public" or "private". If this is set to "private", the field `description` will be returned as `null` on all requests not authorized with this system's token. In addition, this field will be returned as `null` if the request is not authorized with this system's token. Because of this, there is no way for an unauthorized user to tell the difference between a private description and a `null` description - this is intentional. example: public member_list_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The system's member list privacy setting, either "public" or "private". If this is set to "private", this system's member list can not be queried without proper authorization. In addition, this field will be returned as `null` if the request is not authorized with this system's token. example: public front_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The system's current fronter privacy setting, either "public" or "private". If this is set to "private", this system's current fronter can not be queried without proper authorization. In addition, this field will be returned as `null` if the request is not authorized with this system's token. example: public front_history_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The system's front history privacy setting, either "public" or "private". If this is set to "private", this system's front/switch history can not be queried without proper authorization. In addition, this field will be returned as `null` if the request is not authorized with this system's token. example: public Member: properties: id: $ref: "#/components/schemas/ID" name: type: string maxLength: 100 description: The user-provided name of the member. example: "Myriad Kit" display_name: type: string maxLength: 100 description: The member's "display name", which will override the member's normal name when proxying. nullable: true example: Myriad 'Big Boss' Kit description: type: string maxLength: 1000 description: | The user-provided description of the member. May contain rich text in Markdown, including standard Markdown-formatted links, or Discord-formatted emoji/user/channel references. If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`. example: Myriad is very cool and rad, and they love snuggling. color: type: string format: color minLength: 6 maxLength: 6 nullable: true pattern: "^[0-9A-Fa-f]{6}$" description: | The member's "color", displayed on member cards, as a 6-character hexadecimal color code (no leading #). If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`. example: "FF0000" birthday: type: string format: date example: "2018-07-11" nullable: true description: | The user-provided birthdate of the member. "Year-less" birthdays are supported. In this case, the year should be set to `0004`, and that specific year should be special-cased and hidden from the user. Previous versions used the year `0001` for the same purpose, and this value may still be both read and written with the API and should be treated the same as `0004`. The year `0004` was chosen because it is a leap year in the Gregorian calendar, and thus the date `0004-02-29` can be properly represented. If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`. pronouns: type: string maxLength: 100 example: "they/them or xe/xem" nullable: true description: | The user-provided pronouns of the member. There is no specific schema, just a freeform text field. If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`. avatar_url: type: string format: url nullable: true maxLength: 256 example: "https://i.imgur.com/Abcdefg.png" description: | A link to the avatar/icon of the member. If used for proxying, the image must be at most 1000 pixels in width *and* height, and at most 1 MiB in size. This restriction is on Discord's end and is not verified through the API (it's simply stored as a string). privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | This field is deprecated. At the moment, it's still included in member objects, with a value that mirrors the `visibility` field. Writing to this field will set *every* privacy setting to the written value. example: public deprecated: true visibility: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The member's current visibility, either "public" or "private". If this is set to "private", the member will not appear in public member lists. It can still be looked up using its 5-character member ID, but will return limited information. In addition, this field will be returned as `null` if the request is not authorized with this system's token. example: public name_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The member's current name privacy setting, either "public" or "private". If this is set to "private", the member's returned `name` will be replaced by their `display_name` when applicable. In addition, this field will be returned as `null` if the request is not authorized with this system's token. Because of this, there is no way for an unauthorized user to tell the difference between a private name being replaced by the display name, and an empty display name with the name set - this is intentional. example: public description_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The member's current description privacy setting, either "public" or "private". If this is set to "private", the field `description` will be returned as `null` on all requests not authorized with this system's token. In addition, this field will be returned as `null` if the request is not authorized with this system's token. Because of this, there is no way for an unauthorized user to tell the difference between a private description and a `null` description - this is intentional. example: public avatar_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The member's current avatar privacy setting, either "public" or "private". If this is set to "private", the field `avatar_url` will be returned as `null` on all requests not authorized with this system's token. In addition, this field will be returned as `null` if the request is not authorized with this system's token. Because of this, there is no way for an unauthorized user to tell the difference between a private avatar and a `null` avatar - this is intentional. example: public pronouns_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The member's current pronouns privacy setting, either "public" or "private". If this is set to "private", the field `pronouns` will be returned as `null` on all requests not authorized with this system's token. In addition, this field will be returned as `null` if the request is not authorized with this system's token. Because of this, there is no way for an unauthorized user to tell the difference between private pronouns and `null` pronouns - this is intentional. example: public birthday_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The member's current birthday privacy setting, either "public" or "private". If this is set to "private", the field `birthday` will be returned as `null` on all requests not authorized with this system's token. In addition, this field will be returned as `null` if the request is not authorized with this system's token. Because of this, there is no way for an unauthorized user to tell the difference between a private birthday and a `null` birthday - this is intentional. example: public metadata_privacy: allOf: - $ref: "#/components/schemas/PrivacySetting" - description: | The member's current metadata privacy setting, either "public" or "private". If this is set to "private", the field `created` on all requests not authorized with this system's token. In addition, this field will be returned as `null` if the request is not authorized with this system's token. example: public proxy_tags: type: array items: $ref: "#/components/schemas/ProxyTag" description: | An unordered list of the member's proxy tag pairs. It is valid for a member to have any number of proxy tags, including none at all. prefix: type: string nullable: true description: | Previous versions of the API only supported a single proxy tag pair per member. This field will contain the prefix of the first proxy tag registered, or `null` if missing. Setting it will write to the first proxy tag's prefix, creating it if not present. This field is deprecated and will be removed in API v2. deprecated: true example: "{{" suffix: type: string nullable: true description: | Previous versions of the API only supported a single proxy tag pair per member. This field will contain the suffix of the first proxy tag registered, or `null` if missing. Setting it will write to the first proxy tag's suffix, creating it if not present. This field is deprecated and will be removed in API v2. deprecated: true example: "}}" keep_proxy: type: boolean default: false description: | Whether or not to include the used proxy tags in proxied messages. created: type: string format: date-time readOnly: true description: The creation timestamp of the member. May be returned as `null` depending on the value of `metadata_privacy` and the request authorization. nullable: true PrivacySetting: title: Privacy Setting type: string nullable: true description: A privacy setting for systems or members, either public or private. May occasionally be `null` in cases where you are not authorized to view the privacy setting's state. enum: [public, private] example: public ProxyTag: title: Proxy Tag description: | Represents a proxy tag to match messages on. A "proxy tag" consists of a prefix and a suffix, and a given proxy tag set matches a string if that string begins with the prefix and ends with the suffix. It's often represented to the user with the word `text` between them, eg. `[text]`, `{{text`, and so on. For example, the proxy tag pair "[" and "]" will match any string \[in square brackets\]. Either the prefix or the suffix may be missing (or both may be present), but it is invalid for both values to be null. properties: prefix: type: string nullable: true description: | The proxy tag prefix. This is the string that goes *before* the inner text. An empty prefix is represented as `null` and an empty string will be converted as such. example: "{{" maxLength: 100 suffix: type: string nullable: true description: | The proxy tag suffix. This is the string that goes *after* the inner text. An empty suffix is represented as `null` and an empty string will be converted as such. example: "}}" maxLength: 100 Switch: title: Logged Switch properties: timestamp: type: string format: date-time description: The timestamp the switch was logged. readOnly: true members: type: array description: | A list of the IDs of the members in this switch. The order is significant. It is valid for the switch to have no members at all. items: $ref: "#/components/schemas/ID" FullSwitch: title: Logged Switch (with full Member object) properties: timestamp: type: string format: date-time description: The timestamp the switch was logged. members: type: array description: | A list of the members in this switch. The order is significant. It is valid for the switch to have no members at all. items: $ref: "#/components/schemas/Member" Snowflake: title: Snowflake (Discord ID) description: | A unique identifier used by Discord for its objects (accounts, guilds, channels, messages). This is internally stored as a 64-bit unsigned integer, but is represented as a string in the API to accomodate languages that store numbers as floating point values (and thus would not be able to represent it losslessly). type: string pattern: "^[0-9]{17,19}" minLength: 17 maxLength: 19 example: "466378653216014359" # PK's account ID :3 Message: title: Message Info description: | An object containing information about a proxied message, including message, user and channel IDs, as well as the system and member it's related to. For privacy and performance reasons, this endpoint does not return the *contents* of the original message. This data isn't stored in the database either way - but given the channel and message ID, it can be fetched from Discord's own API. properties: timestamp: type: string format: date-time readOnly: true description: The time the message was proxied. id: allOf: - $ref: "#/components/schemas/Snowflake" - description: "The ID of the proxied webhook message posted by PluralKit." example: "123456789012345678" original: allOf: - $ref: "#/components/schemas/Snowflake" - description: "The ID of the original (now-deleted) trigger message containing the proxy tags." example: "123456789012345678" sender: allOf: - $ref: "#/components/schemas/Snowflake" - description: "The ID of the Discord user that sent the trigger message." example: "123456789012345678" channel: allOf: - $ref: "#/components/schemas/Snowflake" - description: "The ID of the Discord channel the relevant messages were posted in." example: "123456789012345678" system: allOf: - $ref: "#/components/schemas/System" - description: "The system that proxied the message." member: allOf: - $ref: "#/components/schemas/Member" - description: "The member that proxied the message." securitySchemes: TokenAuth: type: apiKey in: header name: Authorization description: A system token obtained from the `pk;proxy` command in the Discord bot.