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.
  • Slack and Discord are native destinations too. Paste an incoming webhook URL from your own Slack workspace or Discord channel, and sharedrop posts a plain-text message there.
  • 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.

Team webhooks

Each Team workspace has its own webhooks, separate from anyone's personal ones.

  • Who manages them: the workspace owner, in the workspace sidebar under Team > Webhooks. Members and viewers cannot see or change them.
  • Which events they get: the same four events, for every file in that workspace, whoever uploaded, re-uploaded, deleted, or shared it.
  • What they need: the workspace's own Team plan (paid or complimentary). The owner's personal plan does not matter. If the workspace's Team plan ends, its webhooks stop sending.
  • Personal webhooks (Dashboard > Settings > Webhooks) only get events for your personal files. They never get events for files in a workspace, even one you belong to.

Add a destination

  1. Open Dashboard > Settings > Webhooks and choose Add destination.
  2. Pick Telegram, Slack, Discord, 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.

Slack destination

Post events into a Slack channel through an incoming webhook you create in your own workspace. There is no Sharedrop app to install.

Create the incoming webhook

  1. Follow Slack's guide to sending messages using incoming webhooks: create a Slack app for your workspace, turn on Incoming Webhooks, choose Add New Webhook to Workspace, and pick the channel.
  2. Copy the webhook URL. It looks like https://hooks.slack.com/services/T.../B.../....
  3. In sharedrop, choose Slack, paste the URL, optionally give the destination a name, pick your events, and save.

sharedrop only accepts URLs that start with https://hooks.slack.com/services/. Anything else is refused when you save.

What the message looks like

Each event becomes one plain-text message built from the destination's message template (the same template and variables as Telegram, see Message template). With the default template:

file.uploaded: Q1 Sales Dashboard
https://sharedrop.cloud/agent/x8vh3p
by agent

Slack's special characters are escaped, so a page title such as <!channel> shows up as text and never notifies the channel. Messages are cut at 4000 characters.

Discord destination

Post events into a Discord channel through a webhook you create in your own server.

Create the webhook

  1. Follow Discord's Intro to Webhooks: open the channel's or server's settings, go to Integrations, choose Create Webhook, and pick the channel.
  2. Choose Copy Webhook URL. It looks like https://discord.com/api/webhooks/<number>/<token>.
  3. In sharedrop, choose Discord, paste the URL, optionally give the destination a name, pick your events, and save.

sharedrop accepts URLs on discord.com or discordapp.com with the /api/webhooks/ path. Webhook URLs copied from the Discord PTB or Canary apps (ptb.discord.com, canary.discord.com) are not accepted; change the host to discord.com before you paste it.

What the message looks like

The message is plain text built from the same message template as Telegram and Slack. Mentions are switched off for every message, so a page title containing @everyone, @here, or a role or user mention pings nobody. Messages are cut at Discord's 2000-character limit.

Keeping the webhook URL safe

For both Slack and Discord, the webhook URL is the only thing needed to post to your channel, so treat it like a password. sharedrop stores it encrypted, uses it only on its own servers at send time, and never shows it again. The destination list shows only the host and the last four characters (for example hooks.slack.com/...a1b2). To change the URL, edit the destination and paste the new one; leave the field blank to keep the current URL.

If a Slack or Discord destination fails

When you save, sharedrop sends a "Sharedrop connected" test message and refuses to save if it does not arrive, so a saved destination is a working one. Replacing the URL later runs the same test; renaming or changing events or the template sends nothing. If the test fails:

  • Slack webhook rejected, or Discord could not find the webhook: the webhook was deleted or revoked, or the URL was copied incompletely. Create a new webhook and paste its URL.
  • Slack cannot post to the channel: the channel was archived or deleted, or posting there is restricted. Pick another channel by creating a new webhook.
  • Temporarily unavailable: Slack or Discord had a brief problem. Try again in a moment.

A webhook can also be deleted after you save. When a live event finds the webhook revoked or deleted, or the Slack channel unavailable, the destination is disabled automatically with the reason shown next to it (for example "Disabled: Slack webhook revoked or deleted"). Create a new webhook, edit the destination to paste its URL, 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 all four destination types (Telegram, Slack, Discord, and custom webhooks). 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, Slack, or Discord 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. For Slack and Discord, a revoked or deleted webhook (or an archived or deleted Slack channel) does the same.
  • 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, Slack, and Discord messages carry no such id, so in the rare duplicate case the same notification can appear twice in the chat or channel.