freeq exposes a REST API alongside the IRC and WebSocket interfaces.
https://irc.freeq.at/api/v1
GET /api/v1/health
Returns server status:
{
"server_name": "irc.freeq.at",
"connections": 42,
"channels": 12,
"uptime_secs": 86400
}
GET /api/v1/channels
Returns public channels with member counts:
[
{
"name": "#freeq",
"topic": "Welcome to freeq",
"members": 15,
"modes": "+nt"
}
]
Filters out empty channels with no topic.
GET /api/v1/history/{channel}?limit=50&before={msgid}
Returns recent messages. Requires the channel name without # prefix.
GET /api/v1/verify/{msgid}
Verify a message's cryptographic signature. Returns the signing key, signature, and verification result. verification.proves is what a third party may conclude: authorship when the sender's own registered key signed it, relay when only this server did (the verdict is still valid, but the message is not attributable to the sender), nothing when invalid or uncheckable; verification.meaning says the same in a sentence. A verified_by of key-retired means the key that signed was retired by its owner before the message was made, and the verdict is invalid: the signature is not evidence of anything once the key it names was withdrawn. key_source names where the key that checked the signature came from: server-key for this server's own key, otherwise local-session, origin-server, identity-record, did-document, or unknown for a key filed before sources were recorded; it is absent when no key was found.
GET /api/v1/signing-key
Returns the server's ed25519 public key (base64url-encoded) used for message attestation. kid is the key's id, and registered_at is when this server first filed the key in its own key store, in seconds since the epoch, or null for a server running without a database. expires_at is null: a server's own keys never expire. did is the name the server files its own keys under, did:web:<server-name>; the server's whole key set, current and retired, is at /api/v1/signing-keys/{did} for that value.
GET /api/v1/signing-keys/{did}
Returns the live signing keys an identity has registered: a key past its expiry is left out, and a retired key stays, with its date. public_key is the key it is signing with now — the most recently used key its owner has not retired and that has not expired — or null if there is none. keys lists each key, newest registration first: kid, public_key, registered_at, last_seen_at, removed_at, which is null while the key is not retired and otherwise the time its owner retired it, and expires_at, when the key stops counting, or null for a key that never expires (a server's own key, a bot's did:key). registered_at is when this server first saw the key; for a key copied from the signer's identity records it is the record's createdAt, and for a key copied from a peer server it is the peer's date, or the time of the copy when the peer sent none. Every key expires 90 days after registered_at by default (--signing-key-lifetime-days), except that a key published in the account's records follows its record's expiry. Times are seconds since the epoch. A DID with no keys is a 200 with a null public_key and an empty list, not a 404.
GET /api/v1/signing-keys/{did}/{kid}
Returns the one key that identity registered under kid, with the same window fields. This is the lookup a verifier uses when a signature names its kid: the key stays fetchable after the session that made it ends, after it is retired and after it expires, with those dates. An unknown kid is a 404.
GET /api/v1/signing-keys?keys={did}/{kid},{did}/{kid},…
Returns up to 50 keys in one request, each as {did, kid, public_key, registered_at, last_seen_at, removed_at, expires_at} under keys, in the order asked. A key this server does not hold is left out; expired and retired keys are answered with their dates. For a did:web: key it does not hold, the server first reads that DID's own document (at most five a request; a miss is not asked again for --peer-key-retry-secs). More than 50 pairs (counted before duplicates are dropped), none, or a pair without a / is a 400. It shares the record routes' rate limit (429).
GET /api/v1/blob?url={encoded-pds-url}&mime={encoded-mime}
Proxies PDS blob downloads. Strips Content-Disposition: attachment headers that block browser playback. Supports Range requests for streaming.
GET /api/v1/og?url={encoded-url}
Fetches Open Graph metadata for a URL. Returns title, description, image, and site name. Server-side fetch prevents IP leakage.
POST /api/v1/upload
Authorization: Bearer {web-token}
Content-Type: multipart/form-data
Upload a file to the user's PDS. Returns the blob URL and media attachment tags.
GET /api/v1/pins/{channel}
Returns pinned messages for a channel:
[
{
"msgid": "01ABCDEF...",
"from": "alice",
"text": "Welcome!",
"pinned_by": "bob",
"pinned_at": "2024-01-01T00:00:00Z"
}
]
Most read endpoints are public. Write endpoints (upload, pin) require a web-token from the auth broker, sent as Authorization: Bearer {token}.
Allowed origins: irc.freeq.at, auth.freeq.at, freeq.at, localhost:*.
All responses include:
- Content-Security-Policy (strict)
- Strict-Transport-Security (HSTS)
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Referrer-Policy: strict-origin-when-cross-origin