Zrzur Developers

Interactions over HTTP

Receive commands, buttons and forms at your own HTTPS address, without a gateway connection.

You will need

Steps

  1. Open your bot in the Developer Portal and choose HTTP interactions. Make a signing secret and copy it. It is shown once.
  2. Start the example below on your server, with the secret in ZRZUR_SIGNING_SECRET.
  3. Enter your address in the same dialog, save and confirm your password. Zrzur sends it two test requests first, in either order: one with a wrong signature, which it must refuse, and a signed Ping, which it must answer with {"type":"Pong"}. The SDK does both for you.

From now on the gateway no longer receives this bot's interactions. Remove the address to go back.

Full example

// A bot without a gateway connection: `/hello` answered over HTTP.
//
//   ZRZUR_BOT_TOKEN=... ZRZUR_SIGNING_SECRET=... PORT=8787 node http-interactions.mjs

import { Client } from "@zrzur/sdk/bot";

const bot = new Client({ apiUrl: process.env.ZRZUR_API_URL });

bot.command("hello", (i) => i.reply(`Hello ${i.user.username}, over HTTP!`));

await bot.loginHttp(process.env.ZRZUR_BOT_TOKEN);
await bot.syncCommands([{ name: "hello", description: "Say hello" }]);
await bot.listen({
  secret: process.env.ZRZUR_SIGNING_SECRET,
  port: Number(process.env.PORT ?? 8787),
});
console.log(`ready as ${bot.user.username}`);

What you should see

ready as <your bot> on the console, then Hello <name>, over HTTP! in the channel when someone uses /hello.

What a request looks like

Each request is a POST with the same JSON as InteractionCreate's interaction, and three headers:

HeaderMeaning
X-Zrzur-TimestampSeconds since the epoch when it was signed
X-Zrzur-Signaturev1= and the hex HMAC-SHA256 of the signed text
X-Zrzur-DeliveryId of this delivery, for your logs. Not signed

The signed text is v1:<timestamp>:<raw body>. Without the SDK:

  1. Refuse the request with 401 if the timestamp is more than 300 seconds from your clock.
  2. Compute the HMAC-SHA256 of the signed text with your secret and compare it in constant time (crypto.timingSafeEqual in Node, hmac.compare_digest in Python). Never compare with ==.
  3. Refuse an _id from the signed body that you have already handled in the last ten minutes: that is a captured request sent again. Do not use X-Zrzur-Delivery for this: it is not signed, so anyone can change it.
  4. Answer {"type":"Ping"} with {"type":"Pong"}.

A bot that was disabled, or whose owner was suspended, receives nothing, and its address cannot be changed.

Check against this example. With the secret zrzur_sign_f27c5773e21ddeceaeb5e956dc45c16451f737addd5c0e4472f61060bca9b030, the timestamp 1700000000 and the body {"type":"Ping"}, the signature is v1=928e5ffdc88564103b59f5485a6bfa7e1a633dca64f397396606a314f572c4d3.

How to answer

Answer within 3 seconds, in one of two ways:

Any other status, a timeout, a redirect or an answer over 1 MiB fails the interaction, and the person sees that it failed. Redirects are not followed.

Common errors

Saving an address fails with SignedDeliveryRejected, and reason says why:

reasonCauseFix
invalid_urlNot https, or holds a passwordUse a plain https:// address
blocked_addressThe address is private or blockedUse a public address
not_reachableNothing answeredCheck that the server runs and is public
timed_outNo answer in 3 secondsAnswer faster
accepts_bad_signaturesThe request with a wrong signature passedCheck signatures, answer 401
bad_statusThe Ping did not get 200Answer Ping with 200
bad_answerThe Ping did not get {"type":"Pong"}Send exactly that JSON
answer_too_largeThe answer is over 1 MiBSend a smaller answer

FeatureDisabled means this server has no signing key configured, or one under 32 bytes. InvalidOperation means the bot has no signing secret yet.

Next