Developers

Build a plugin for Quid

Add slash commands and automations that teams can install in one click. A plugin is a small web service you run; Quid talks to it over signed HTTPS. No SDK required: any language that can serve JSON works.

Slash commands

Reply privately or post to the channel.

Events

React to new messages in channels you’re added to.

Safe by design

Signed requests, scoped tokens, no code in Quid.

How plugins work

  1. 1SubmitRegister your plugin in Quid with its URL and commands. You get a signing secret once.
  2. 2ChecksQuid runs automatic safety checks. Pass them and you’re listed as a community plugin.
  3. 3InstallA workspace admin installs it into specific channels. You receive an install token.
  4. 4RunPeople type your commands; Quid calls your server, and you reply or post messages.

No plugin code runs inside Quid or in people’s browsers. Your plugin only ever sees the channels an admin added it to, and admins can remove it at any time.

Quickstart

This is a complete plugin in one file with no dependencies (Node 18+). It answers /hello in any channel it’s added to.

server.mjs
// server.mjs: a complete Quid plugin with one command, /hello.
// Run: QUID_SIGNING_SECRET=qps_... node server.mjs
import { createHmac, timingSafeEqual } from 'node:crypto'
import { createServer } from 'node:http'

const SECRET = process.env.QUID_SIGNING_SECRET ?? ''
const installs = new Map() // installId -> { token, apiBase } (use a database in production)

function signedByQuid(req, raw) {
  const ts = req.headers['x-quid-timestamp']
  const sig = req.headers['x-quid-signature'] ?? ''
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
  const want = Buffer.from('sha256=' + createHmac('sha256', SECRET).update(ts + '.' + raw).digest('hex'))
  const got = Buffer.from(sig)
  return want.length === got.length && timingSafeEqual(want, got)
}

createServer(async (req, res) => {
  let raw = ''
  for await (const chunk of req) raw += chunk
  const body = JSON.parse(raw || '{}')
  const reply = (status, json) => {
    res.writeHead(status, { 'content-type': 'application/json' })
    res.end(JSON.stringify(json ?? {}))
  }

  // Quid checks you own the server before you have a secret, so this one is unsigned.
  if (req.url === '/quid/verify') return reply(200, { challenge: body.challenge })
  if (!signedByQuid(req, raw)) return reply(401, { error: 'bad signature' })

  switch (req.url) {
    case '/quid/install':
      installs.set(body.installId, { token: body.token, apiBase: body.apiBase })
      return reply(200)
    case '/quid/uninstall':
      installs.delete(body.installId)
      return reply(200)
    case '/quid/command':
      if (body.command === 'hello')
        return reply(200, { visibility: 'channel', text: `👋 Hi **${body.user.name}**!` })
      return reply(200, { visibility: 'ephemeral', text: 'Unknown command' })
    default:
      return reply(404, { error: 'not found' })
  }
}).listen(3000, () => console.log('Plugin listening on :3000'))
  • Put it on a public HTTPS address (Cloud Run, Fly, Render, a VPS…).
  • In Quid, open Plugins → Submit a plugin, enter that address as the base URL and add a hello command.
  • Copy the signing secret you’re shown into QUID_SIGNING_SECRET and restart.
  • Install it into a channel and type /hello.

Submitting

A submission has:

FieldRules
Name2–40 characters. Can’t use “Quid” or “official”.
Tagline4–90 characters, plain text, no links.
DescriptionUp to 2,000 characters.
Developer name and emailThe name is shown on the listing; the email is only used to contact you.
Base URLPublic HTTPS. Quid calls {baseUrl}/quid/….
Icon URLOptional PNG, JPEG, WebP or GIF under 512 KB. Copied into Quid.
CommandsUp to 10. Names are 2–32 lowercase letters, numbers or dashes.
Eventsmessage.created
PrivateEnterprise+ workspaces can keep a plugin to themselves.

Automatic safety checks run right away:

  • The base URL is HTTPS on a public address (private networks are refused).
  • Your server echoes a one-time challenge, proving you control it.
  • The icon is a real image under 512 KB (SVG is refused).
  • The listing doesn’t claim to be official or verified and contains no links or HTML.
  • None of your URLs are on Google Safe Browsing’s list of dangerous sites.

Verifying requests

Every request from Quid (except /quid/verify) carries two headers:

headers
x-quid-timestamp: 1791234567
x-quid-signature: sha256=<hex HMAC-SHA256(signing secret, timestamp + "." + raw body)>

Compute the HMAC over the exact raw body bytes, compare in constant time, and reject timestamps more than 5 minutes old. The Node version is in the quickstart; here it is in Python:

verify.py
import hashlib, hmac, time

def signed_by_quid(headers, raw_body: bytes, secret: str) -> bool:
    ts = headers.get("x-quid-timestamp", "")
    sig = headers.get("x-quid-signature", "")
    if not ts or abs(time.time() - int(ts)) > 300:
        return False
    want = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, sig)

Requests from Quid

Quid sends POST requests with a JSON body to these paths under your base URL:

PathWhenReply
/quid/verifyDuring the safety checks{ "challenge": "<same value>" }
/quid/installAdded to a workspace. Body has installId, workspace, channels, token, apiBaseAny 2xx. Store the token
/quid/uninstallRemoved from a workspaceAny 2xx. Forget the token
/quid/commandSomeone ran one of your commandsWithin 5 s, see below
/quid/eventsA subscribed event happenedAny 2xx (retried with backoff)

Slash commands

When someone types /poll Lunch? | Pizza | Sushi, you receive:

POST /quid/command
{
  "type": "command",
  "installId": "9b2e…",
  "command": "poll",
  "text": "Lunch? | Pizza | Sushi",
  "user": { "id": "…", "name": "Ada Park" },
  "channel": { "id": "…", "name": "general" },
  "threadId": null
}

Reply within 5 seconds with:

response
{ "visibility": "channel", "text": "📊 **Lunch?**\n- Pizza\n- Sushi" }
  • ephemeral shows the text only to the person who ran the command (good for errors and help).
  • channel posts it as your plugin for everyone to see.
  • Need longer? Reply ephemeral right away (“Working on it…”) and post the result later with the API.

Posting messages

Use the install token and apiBase you received at install time. Plugins can post only in channels the admin added them to.

POST {apiBase}/messages
curl -X POST "$API_BASE/messages" \
  -H "Authorization: Bearer $INSTALL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "channelId": "4f1c…", "text": "Deploy finished ✅", "threadId": null }'

Text is up to 4,000 characters. Pass a threadId to reply in a thread.

Events

Subscribe to message.created to see new messages in your channels (the listing tells admins you can read them). Deliveries are retried with backoff if you don’t answer 2xx, so make handlers idempotent using message.id.

POST /quid/events
{
  "type": "event",
  "event": "message.created",
  "installId": "9b2e…",
  "message": {
    "id": "…", "channelId": "…", "authorId": "…", "authorName": "Ben Ortiz",
    "text": "ship it", "threadId": null, "createdAt": "2026-10-07T09:12:00.000Z"
  }
}

Formatting

Plugin messages support a small, safe subset of Markdown:

WriteShows as
**bold**bold
`code`code
- item• item (a bullet list)
EmojiAs is 🎉

Review and badges

Community

Passed the automatic checks. Enterprise+ workspaces can install after a warning.

Verified

Reviewed by the Quid team. Any workspace can install it.

Private

Only the workspace that submitted it can see and install it.

The Official badge is reserved for Quid’s own plugins. The team can suspend a plugin that misbehaves, which stops it everywhere at once.

Limits

WhatLimit
Command reply5 seconds
Posting messages60 per minute per install
Message length4,000 characters
Commands per plugin10
Signature window5 minutes

Testing locally

Quid only calls public HTTPS addresses. While developing, expose your local server with a tunnel such as cloudflared tunnel --url http://localhost:3000 or ngrok http 3000, and submit the tunnel’s URL as a private plugin in your own workspace. Update the base URL when you deploy for real.

Ready to build?

Submit from Plugins in your workspace. Questions? Email us.

Open Quid