Webhooks: not built yet
Dium does not send webhooks to your server today. Here is what works now, and the design we are proposing.
What exists today
Dium does not send outbound webhooks to partner servers today. Here is what you can use instead.
| Mechanism | Direction | Who can use it |
|---|---|---|
| Echo real-time rooms | Dium to browsers | The Dium app, including the app you mount on your site |
| 60-second polling | Browser to Dium | The Dium app, as a backup for Echo |
Moat profile callback at /user/update.php | Moat to Dium | Moat only. Not open to partners |
| Partner API | Your server to Dium | You, with a key. See the API reference |
Echo rooms
Echo is the service that pushes live updates to the Dium app in the browser. It tries a WebSocket first, then long polling, then short polling every 3 seconds, then a CDN poll. On reconnect it resumes from the last message it saw.
| Room | Carries |
|---|---|
wave_{{id}} | New replies and changes on a Wave |
live_{{waveId}} | Typing and presence during a live session |
dm_{{userId}} | New direct messages for a member |
asq:page:{{exhibitId}} | The wall on a sponsor or exhibitor Page |
flow_{{ladderId}} | Flow-wide announcements |
If you mount Dium on your site, your members get these updates with no extra work. Echo is not a public event stream for your server, and room names are not a way to authorise anyone.
Polling
Even with Echo, the app checks for unread notifications and DMs every 60 seconds, and checks for new replies on the Wave a member is reading. If your backend needs to know about new content today, poll the API on a schedule and keep within the rate limits.
The Moat profile callback
When a member edits their Moat profile, Moat sends a form-encoded POST to /user/update.php with an X-API-Key header. Dium checks the key in constant time, finds the member by public_key (then email), updates the name and avatar, and merges the rest into the stored profile. Email changes are logged and not applied. The response is {"success", "user_id", "fields_updated"}.
This is an inbound callback from one trusted service. It is not a webhook you can subscribe to.
Proposal: outbound webhooks
If we build webhooks, we plan to follow the open Standard Webhooks spec so you can reuse existing verification libraries.
Headers
| Header | Value |
|---|---|
webhook-id | Unique message ID. Use it to drop duplicates. |
webhook-timestamp | Unix time in seconds when the message was signed. |
webhook-signature | Space-separated list such as v1,<base64>. More than one entry during secret rotation. |
Signing
HMAC-SHA256 over webhook-id + "." + webhook-timestamp + "." + raw body, keyed with your endpoint secret (whsec_ plus base64). Verify against the raw request body, before any JSON parsing. Compare in constant time.
// PROPOSAL: how verification would work under the Standard Webhooks spec.
// Dium does not send webhooks today.
import crypto from "node:crypto";
export function verifyDiumWebhook(rawBody, headers, secret) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const sigs = headers["webhook-signature"];
if (!id || !ts || !sigs) return false;
// Reject old or future messages. Never set the tolerance to 0.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto.createHmac("sha256", key)
.update(`${id}.${ts}.${rawBody}`) // raw body, byte for byte
.digest("base64");
return sigs.split(" ").some((entry) => {
const [version, sig] = entry.split(",");
if (version !== "v1" || !sig) return false; // ignore unknown schemes
const a = Buffer.from(sig), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}Payload
{
"type": "answer.best_marked",
"timestamp": "2026-10-14T14:05:00Z",
"data": { "flow_id": 42, "wave_id": 901, "answer_id": 5531 }
}Candidate events
Based on the notifications Dium already creates. Tell us which ones matter to you.
| Proposed event | Would fire when |
|---|---|
wave.created | A new Wave is posted in a Flow |
answer.created | Someone replies to a Wave |
answer.best_marked | A best answer is marked and the Wave becomes solved |
page.created | A sponsor, exhibitor, speaker or partner Page is created |
member.join_requested | Someone asks to join a Flow that needs approval |
flow.suspended | A Flow is suspended or restored |
Delivery, retries and ordering
- Your endpoint answers with any 2xx within 10 seconds. Anything else counts as a failure, including redirects.
- Failed messages are retried with growing gaps over several days, each retry with a fresh timestamp and signature.
- Delivery is at least once, so the same
webhook-idcan arrive twice. Store IDs you have handled. - Order is not guaranteed. If order matters, fetch the current record from the API.