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
- Base URL:
https://discordpgp.com - All requests and responses are JSON (
Content-Type: application/json). - Errors return a non-2xx status with
{ "error": "human readable reason" }. - The server only ever sees armored OpenPGP material: public keys and ciphertext. Never plaintext, never private keys.
- Free for reasonable use. Be gentle; heavy abuse gets rate-limited or blocked.
curl -s https://discordpgp.com/api/keys/745277370465910875Keys
The key directory maps Discord user ids to armored OpenPGP public keys.
/api/keys/:discordUserIdFetch one user's public key. 404 if none is registered.
{ "id": "745277370465910875",
"publicKey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." }/api/keys/lookupBatch 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 } }/api/resolveReverse 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.
/api/messagesStore 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" }/api/msg/:idPublic 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": "…" } }/msg/:idThe 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.
/api/presenceAnnounce 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" }/api/presence/:channelIdEveryone currently present in a channel, with their keys (max 100).
{ "recipients": [
{ "id": "745277370465910875", "pem": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." } ] }Profiles
/api/u/:usernameA 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.
/api/pgp/challengeGet 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…" }/api/pgp/loginSend 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
- Endpoints that mutate your own account (registering a key, editing a profile) require a browser session (Discord OAuth or PGP sign-in) and aren't part of the public surface.
- Everything here is what the open-source extension uses; read it for working client code.
- No stability promise yet: the API is young and may evolve. Breaking changes will bump the envelope
v.