⚙️ Developers

Public API

Everything the extension does goes through this free HTTP API, and you can use it too: look up OpenPGP keys by Discord user id, store and fetch encrypted messages, and discover who can receive encrypted mail in a channel. No API key, no signup. CORS is open on all /api/* routes.

Basics

curl -s https://discordpgp.com/api/keys/745277370465910875

Keys

The key directory maps Discord user ids to armored OpenPGP public keys.

GET/api/keys/:discordUserId

Fetch one user's public key. 404 if none is registered.

{ "id": "745277370465910875",
  "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." }
POST/api/keys/lookup

Batch lookup, up to 25 ids per call. Unknown ids come back as null.

// request
{ "ids": ["745277370465910875", "123456789012345678"] }

// response
{ "keys": {
    "745277370465910875": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
    "123456789012345678": null } }
POST/api/resolve

Reverse lookup: which account owns this exact public key?

// request
{ "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." }

// response
{ "id": "745277370465910875", "username": "kysan" }

Messages

Messages are opaque envelopes: an armored OpenPGP message (encrypted client-side, to up to 50 recipients) plus routing metadata. Storing one returns a short link that the wire format embeds in Discord.

POST/api/messages

Store an envelope. pgp must be an armored PGP MESSAGE (max 200 kB); 1 to 50 recipient ids.

// request
{ "v": 3,
  "from": "745277370465910875",
  "recipients": ["123456789012345678"],
  "pgp": "-----BEGIN PGP MESSAGE-----\n..." }

// response
{ "id": "kf83jd02mc", "url": "https://discordpgp.com/msg/kf83jd02mc" }
GET/api/msg/:id

Public metadata for the message viewer: the ciphertext, sender and recipient profiles, and signature info. Never any plaintext.

{ "id": "kf83jd02mc",
  "from": { "id": "…", "username": "kysan", "displayName": "Kysan", "avatarUrl": "…", "accent": "#7b86ff", "effect": "snow", "…": "…" },
  "recipients": [{ "id": "…", "username": "airbel" }],
  "pgp": "-----BEGIN PGP MESSAGE-----\n...",
  "createdAt": 1783800000,
  "signature": { "state": "signed", "from": "…" } }
GET/msg/:id

The share link itself. Browsers get the HTML viewer; send Accept: application/json (or X-DiscordPGP: 1) to get the raw stored envelope instead, exactly as posted.

Channel presence

Recipient discovery works by rendezvous: clients announce "I'm in this channel and here's my key", and senders encrypt to everyone present. Entries expire after 14 days without a refresh.

POST/api/presence

Announce presence. The key must already be registered in the directory.

// request
{ "channelId": "1391234567890123456",
  "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." }

// response
{ "ok": true, "id": "745277370465910875" }
GET/api/presence/:channelId

Everyone currently present in a channel, with their keys (max 100).

{ "recipients": [
    { "id": "745277370465910875", "pem": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." } ] }

Profiles

GET/api/u/:username

A public profile as shown at pgp.bio/<username>, including the owner's public key. origin says which product the account was created through.

{ "profile": {
    "username": "kysan", "displayName": "Kysan", "bio": "…",
    "avatarUrl": "…", "bgUrl": "…", "bgType": "image|video|scene",
    "accent": "#7b86ff", "effect": "snow", "audioUrl": "…",
    "socials": [{ "type": "github", "url": "…" }], "views": 42,
    "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
    "keyUpdatedAt": 1783800000, "origin": "discord" } }

PGP sign-in

The auth used on pgp.bio: prove you own a key by signing a one-time challenge. Handy if you want "login with PGP" in your own tooling against our namespace.

POST/api/pgp/challenge

Get a challenge to sign. Valid for 10 minutes; token is the server's HMAC over it.

{ "challenge": "Sign in to pgp.bio\n\n…\nNonce: …\nExpires: 1783800600",
  "token": "hmac…" }
POST/api/pgp/login

Send back the challenge, its token, your public key, and an armored cleartext signature of the exact challenge text. First login claims username; later logins can omit it. Sets a session cookie.

// request
{ "publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
  "username": "kysan",
  "challenge": "…", "token": "…",
  "signature": "-----BEGIN PGP SIGNED MESSAGE-----\n..." }

// response
{ "ok": true, "username": "kysan" }

Good to know