DocumentationIntegrations
Rust · 0.1.0
Webhooks
Deliver selected memo events to a receiver you control.
Configure and test
Create a personal webhook in Settings with a destination you trust. Webhook payloads can contain memo content, so check what the receiver stores and who can access it. Use a non-sensitive test memo first. Private-network destinations are restricted unless an operator explicitly allows them; do not broadly enable internal-network access to solve a single delivery failure.
Verify signatures and responses
When a signing secret is configured, deliveries include webhook-id, webhook-timestamp and webhook-signature. The signature uses HMAC-SHA256 over id.timestamp.raw-body, encoded as v1,<base64>. A whsec_ secret contains a base64-encoded key. Verify against the unchanged body, compare safely, and reject old/replayed deliveries. The current receiver contract expects a successful HTTP response with valid JSON; if a code field exists, it must be numeric 0. An empty 204 response is not a successful JSON acknowledgement for this implementation.
Plan for best-effort delivery
The dispatcher uses bounded, in-memory work queues and timeouts. A full queue can drop a delivery; this is not a durable exactly-once event log. Make the receiver idempotent, monitor delivery failures, and use periodic API reconciliation when missing an event would matter.
Connect one receiver and one test memo
Prerequisites: an HTTPS receiver you control, a way to retain the untouched request bytes, and protected receiver logs. In Settings → Webhook, create a webhook with a recognizable name and the exact receiver URL. Copy its signing secret into the receiver’s secret store; if it was not shown, reopen the webhook’s edit dialog and reveal the stored secret. Create a private, non-sensitive test memo in the same account. Inspect the received JSON for activityType, creator and memo, then compare it to that test memo. Do not use a public request-inspection service for real private content.
A bounded Node.js signature verifier
This function checks the signature against raw bytes and rejects timestamps more than five minutes away from the receiver’s clock. Run it before parsing or processing the payload. It is a verification building block, not a complete HTTP server: your receiver must also limit request size, reject reused webhook-id values through a durable deduplication store, and safely record accepted work. Use the Node.js crypto API for HMAC and constant-time comparison.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook({ id, timestamp, signature, rawBody, secret },
nowSeconds = Math.floor(Date.now() / 1000)) {
if (typeof id !== "string" || !id ||
typeof timestamp !== "string" || !/^\d+$/.test(timestamp) ||
typeof signature !== "string" || typeof secret !== "string" || !secret ||
!Buffer.isBuffer(rawBody)) return false;
const sentAt = Number(timestamp);
if (!Number.isSafeInteger(sentAt) || Math.abs(nowSeconds - sentAt) > 300) return false;
const key = secret.startsWith("whsec_")
? Buffer.from(secret.slice(6), "base64")
: Buffer.from(secret, "utf8");
if (!key.length) return false;
const digest = createHmac("sha256", key)
.update(`${id}.${timestamp}.`, "utf8")
.update(rawBody)
.digest("base64");
const expected = Buffer.from(`v1,${digest}`, "utf8");
const received = Buffer.from(signature, "utf8");
return expected.length === received.length && timingSafeEqual(expected, received);
}Acknowledge and diagnose delivery
After signature/replay checks and successful recording of the event, return HTTP 200 with Content-Type: application/json and the body below. Do not acknowledge before your receiver has safely accepted the work. A valid signature with changed JSON whitespace should fail if the raw body was changed; test that rejection and an expired timestamp locally. No request at the receiver suggests destination/outbound policy, DNS or connectivity. A request followed by a Memos delivery error suggests status, response JSON, signature handling or receiver timeout. Fix the failing layer, and reconcile through the API if an event may have been dropped.
{"code":0}