Register commands
Global commands work in every server the bot is in; server commands work only there and instantly (great for testing). PUT replaces the whole list of that scope. Types: 1 chat (/name), 2 user menu, 3 message menu. Options: 3 string, 4 integer, 5 boolean, 6 user, 7 channel, 8 role, 10 number; with choices (up to 25), minValue/maxValue, minLength/maxLength or autocomplete. Limits: 100 chat, 15 user and 15 message commands per scope.
PUT /api/v1/applications/APP_ID/commands
[
{ "name": "dado", "description": "Rola um dado",
"options": [{ "type": 4, "name": "lados", "description": "Quantos lados", "minValue": 2, "maxValue": 100 }] },
{ "name": "jogo", "description": "Escolhe um jogo",
"options": [{ "type": 3, "name": "nome", "description": "Nome", "autocomplete": true }] },
{ "name": "kick", "description": "Só admins", "defaultMemberPermissions": "0" },
{ "type": 2, "name": "Ver perfil" }
]
defaultMemberPermissions hides the command from people without those permissions ("0" = administrators only).
The interaction
The gateway delivers INTERACTION_CREATE with id, token, type (2 command, 3 component, 4 autocomplete, 5 modal), user, member (roles and permissions), channelId, serverId and data with already-typed options.
Respond
POST /api/v1/interactions/{id}/{token}/callback, with no Authorization (the token is the credential), exactly once and within 3 seconds:
4message (withflags: 64it is ephemeral: only the invoker sees it, and it never enters channel history).5deferred: “thinking…” shows up; edit it later withPATCH /webhooks/{appId}/{token}/messages/@original.6acknowledges a click without changes ·7edits the component’s message.8autocomplete choices (up to 25) ·9opens a modal.
After the initial response the token stays valid for 15 minutes for follow-ups (POST /webhooks/{appId}/{token}) and to edit or delete those messages. If the app doesn’t answer in time, the person sees “The interaction failed”.
Buttons, menus and modals
Rows (type 1) hold up to 5 buttons or 1 select menu. Buttons: 1 primary, 2 secondary, 3 success, 4 danger (all with customId) and 5 link (with url, no interaction). String selects: up to 25 options, minValues/maxValues. Modals: a title and up to 5 text fields (short or paragraph), one per row.
Receive over HTTP
Save an interactions URL in the portal and interactions arrive as POST requests to your server, signed with Ed25519: verify X-Signature-Ed25519 over X-Signature-Timestamp + body with the app’s public key and answer 401 when it doesn’t match. When you save, Falafy sends a PING (answer {"type":1}) and a request with a wrong signature (it must get 401). The response can come in the HTTP response body itself.
import crypto from 'node:crypto';
import http from 'node:http';
const PUBLIC_KEY = process.env.FALAFY_PUBLIC_KEY; // hex, no portal
const spki = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), Buffer.from(PUBLIC_KEY, 'hex')]);
const key = crypto.createPublicKey({ key: spki, format: 'der', type: 'spki' });
http.createServer((req, res) => {
let raw = '';
req.on('data', (c) => (raw += c));
req.on('end', () => {
const sig = req.headers['x-signature-ed25519'];
const ts = req.headers['x-signature-timestamp'];
const ok = sig && ts && crypto.verify(null, Buffer.from(ts + raw), key, Buffer.from(sig, 'hex'));
if (!ok) return res.writeHead(401).end();
const i = JSON.parse(raw);
res.writeHead(200, { 'content-type': 'application/json' });
if (i.type === 1) return res.end(JSON.stringify({ type: 1 })); // PING -> PONG
res.end(JSON.stringify({ type: 4, data: { content: 'Olá!', flags: 64 } }));
});
}).listen(8080);