Webhooks

Webhooks let you react the moment something happens to your files. When you upload, re-upload, delete, or share a page, sharedrop delivers the event to a destination you choose:

  • Telegram is a native destination. sharedrop formats the event and sends it straight to a Telegram chat or channel. There is no relay to build and no server to run.
  • Custom webhook is a signed HTTPS POST to your own endpoint. Use it to sync a database, notify a channel, or kick off any downstream job you control.

Webhooks are available on Pro and above. Free accounts see an upgrade prompt in Dashboard > Settings > Webhooks.

Events

Every delivery is one of four events.

EventFires when
file.uploadedA new page is created (the first upload of a file).
file.reuploadedAn existing page is re-uploaded (a new version, same stable URL). The payload sets data.page.was_reupload to true.
file.deletedA page is deleted. The payload carries a snapshot of the page taken just before removal.
access.grantedA page is shared with someone by email, creating an access grant. Delivered best-effort alongside the recipient's gated invitation email; delivery is not guaranteed.

Add a destination

  1. Open Dashboard > Settings > Webhooks and choose Add destination.
  2. Pick Telegram or Custom webhook.
  3. Choose which events the destination should receive.
  4. Fill in the details for that destination type (below) and save.

You can disable a destination at any time, and delete it when you no longer need it.

Telegram destination

Deliver events straight into a Telegram chat or channel. sharedrop keeps your bot token encrypted and sends on your behalf, so there is nothing to host and no signature to verify on your side.

Set up your bot

  1. In Telegram, open @BotFather, send /newbot, and copy the bot token it gives you. It looks like 123456789:AA....
  2. Decide where messages should land, and give your bot access:
    • Channel or group: add your bot as an administrator with permission to post messages.
    • Direct chat: open your bot and send it any message first. A bot cannot start a conversation, so it can only message you after you have messaged it.
  3. Get the chat id:
    • For a public channel, use its @username (for example @myreports).
    • Otherwise paste the bot token into the form and use Detect chat, which lists the chats your bot can see so you can pick the right one. Channel and group ids are negative (for example -1001234567890); a direct chat id is a positive number.

The token you paste and the bot you added to the chat must be the same bot. Telegram's admin search matches bots by display name, so it is easy to add a different bot with a similar name by mistake. That is the most common reason a destination will not save.

Message template

Each Telegram destination has a message template. Write plain text with {{variable}} placeholders and sharedrop fills them in for every event. The default template is:

{{event}}: {{page.title}}
{{page.url}}
by {{actor.username}}

These are the variables you can use:

VariableValue
{{event}}The event name, for example file.uploaded
{{page.title}}The page title
{{page.url}}The page's public URL
{{page.kind}}The document kind, for example html
{{page.visibility}}public, private, or shared
{{page.file_size}}File size in bytes
{{actor.username}}Who triggered the event
{{created_at}}When the event fired (ISO 8601)

Anything outside this list renders as empty text. Templates are data only: there are no conditionals, loops, or function calls, so nothing in a template can execute. Messages are sent as plain text and truncated to Telegram's 4096-character limit. The dashboard shows a live preview as you edit.

Security

Your bot token is stored encrypted and is only ever decrypted server-side, at send time, on sharedrop's own infrastructure. It never reaches the edge network, never appears in a log, and is never returned by the API. A message template can only read the event fields listed above, so it can never reach the token.

If a Telegram destination fails

When you save, sharedrop sends a "Sharedrop connected" test message and blocks the save if Telegram rejects it, so a saved Telegram destination is a working one. If the test fails:

  • Bot token rejected: copy the token again from @BotFather.
  • Chat not found: check the chat id (channel and group ids are negative), and make sure your bot has been added to that chat.
  • Cannot post to that chat: give the bot permission to post messages in the channel or group.

Permissions can also change after a destination is saved (for example the bot is removed from a channel, or its posting rights are revoked). When a live event hits one of these permanent reasons, that bad token, chat not found, or cannot post, the destination is disabled automatically with the reason shown next to it. Fix the cause, then re-enable it.

Custom webhook

A custom webhook sends a signed HTTPS POST to your own URL for each event. Verify the signature, then do whatever you need with the payload.

Register the endpoint

  1. Choose Custom webhook and enter your endpoint URL. It must use https (plain http and private or loopback addresses are rejected at registration to prevent server-side request forgery).
  2. Choose which events the endpoint should receive.
  3. Copy the signing secret. It starts with whsec_ and is shown once, right after you create the endpoint. Store it somewhere safe (an environment variable on your receiver). If you lose it, rotate the secret from the same screen to mint a new one.

Payload

Every delivery body is a versioned JSON envelope:

{
  "version": "1",
  "id": "5f3c9b1e-7a42-4c8d-9f0b-2e6a1d5c8b90",
  "event": "file.uploaded",
  "created_at": "2026-07-08T06:34:22.460Z",
  "data": {
    "page": {
      "id": "pg_2x8vh3p",
      "slug": "x8vh3p",
      "title": "Q1 Sales Dashboard",
      "kind": "html",
      "mode": "interactive",
      "visibility": "public",
      "file_size": 48213,
      "content_type": "text/html",
      "url": "https://sharedrop.cloud/agent/x8vh3p",
      "workspace_id": null,
      "was_reupload": false
    }
  },
  "actor": {
    "user_id": "user_2abc123",
    "username": "agent"
  }
}

The top-level version field is currently "1". It exists so new event types (for example share.created or link.expired) can slot in later without breaking receivers. Branch on event, and treat any unknown event as a no-op rather than an error.

The id is a UUID that is unique per event. It also travels in the X-ShareDrop-Delivery header and is your idempotency key (see Delivery and retries below).

Headers and signature

Each delivery carries these headers:

HeaderValue
X-ShareDrop-Signaturet=<unix>,v1=<hex> (the timestamp used for signing plus the HMAC digest)
X-ShareDrop-TimestampThe Unix timestamp (seconds) the payload was signed
X-ShareDrop-EventThe event name, for example file.uploaded
X-ShareDrop-DeliveryThe event id, your idempotency key

The v1 value is an HMAC-SHA256 hex digest computed over the exact string ${t}.${rawBody}, where t is the timestamp and rawBody is the exact raw request body bytes. Verify over the raw bytes you receive, not over a re-serialized copy of the parsed JSON. Re-encoding can change key order or whitespace, and the signature will never match.

Verify a webhook

The check has three parts: recompute the HMAC over ${t}.${rawBody}, compare it in constant time, and reject anything older than a 5-minute skew window (that skew check is what stops an attacker from replaying a captured request). This Node example does all three.

import { createHmac, timingSafeEqual } from "node:crypto";
// Verify a ShareDrop webhook. rawBody is the exact request body string.
export function verify(rawBody: string, header: string, timestampHeader: string, secret: string) {
  const ts = Number(timestampHeader);
  if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5 min skew
  const sent = header.split(",").find((p) => p.startsWith("v1="))?.slice(3) ?? "";
  const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const a = Buffer.from(sent, "hex"), b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Read the raw body before your framework parses it. In an Express handler, use express.raw({ type: "application/json" }) so req.body is a Buffer, then pass req.body.toString("utf8") as rawBody. Pass X-ShareDrop-Signature as header and X-ShareDrop-Timestamp as timestampHeader. Only parse the JSON after verify returns true.

Delivery and retries

Delivery is best-effort and at-least-once, and this applies to both destination types. Behavior depends on why an attempt fails:

  • Transient failures (a custom endpoint that does not return a 2xx, a network error, or a temporary Telegram 5xx or rate limit) are retried with exponential backoff, up to about 5 attempts spread over roughly half an hour. A destination that keeps failing that way is then disabled automatically, with the reason shown next to it. Re-enable it once the cause is fixed.
  • Permanent failures are not retried. For a custom endpoint a redirect (3xx) is treated as a failed attempt (sharedrop never follows redirects). For Telegram, a bad token, chat not found, or cannot-post permission error disables the destination immediately with the reason shown.
  • A hiccup handing the event to our own delivery pipeline (rare) is picked up by a background reconciler within a few minutes rather than being lost, so a brief outage on our side does not silently drop your events.

Because delivery is at-least-once, the same event can arrive more than once (a retry after your receiver accepted but timed out, or a re-send after our result callback was lost, for example). For custom webhooks, deduplicate on the X-ShareDrop-Delivery header (equal to the envelope id): record ids you have already processed and skip repeats. Return a 2xx quickly and do the heavy work asynchronously so you do not trip the retry path. Telegram messages carry no such id, so in the rare duplicate case the same notification can appear twice in the chat.