Skip to content
Slingdrop

Sharing & feedback

Webhooks

Get signed HTTPS callbacks when drops are viewed, commented on, approved and more.

A webhook is an HTTPS endpoint of yours that Slingdrop calls when something happens to your drops: a file finishes uploading, someone opens a link, leaves a comment or signs off. Each delivery is a JSON POST, signed with a secret only you and Slingdrop know, and retried for about a day if your endpoint doesn't answer.

Create a webhook#

Webhooks are managed with your API key. You can also create them in Settings → Developer. An account can have up to 20.

POST/webhooksCreate a webhook (the secret is returned once)
urlstringrequired
Your endpoint. Must be https://, at most 500 characters, and on the public internet (localhost and private ranges are rejected).
eventsstring[]required
One or more event names from the list below.
organizationIdstring
A team id, to also receive events for drops sent as that team.
curl
curl https://api.slingdrop.my/v1/webhooks \
  -H "Authorization: Bearer $SLINGDROP_API_KEY" \
  -H "content-type: application/json" \
  -d '{"url":"https://example.com/hooks/slingdrop","events":["drop.viewed","comment.created","approval.created"]}'

The response carries the signing secret. Keep it with your other secrets; it starts with whsec_.

201 Created
{
  "id": "…",
  "url": "https://example.com/hooks/slingdrop",
  "events": ["drop.viewed", "comment.created", "approval.created"],
  "secret": "whsec_…"
}

List, delete and inspect deliveries#

GET/webhooksYour webhooks, plus the list of event names
DELETE/webhooks/{id}Delete a webhook
GET/webhooks/{id}/deliveriesThe last 100 delivery attempts

GET /webhooks returns { data: [{ id, url, events, active, createdAt }], events: [...] }. Each delivery record is { id, event, statusCode, attempt, deliveredAt, createdAt }: statusCode is null when your endpoint couldn't be reached, and deliveredAt is set only for a 2xx answer.

SDK
const { data, events } = await sd.webhooks.list();
const attempts = await sd.webhooks.deliveries(data[0].id);
await sd.webhooks.delete(data[0].id);

What a delivery looks like#

Every event is sent as the same envelope. data depends on the event.

request
POST /hooks/slingdrop HTTP/1.1
content-type: application/json
user-agent: Slingdrop-Webhooks/1.0
slingdrop-event: comment.created
slingdrop-signature: t=1767225600,v1=5f2b…c9

{"id":"evt_…","type":"comment.created","createdAt":"2026-01-01T00:00:00.000Z","data":{…}}
idstring
Unique per event (evt_…). A retried delivery keeps the same id, so use it to ignore duplicates.
typestring
The event name, also sent in the Slingdrop-Event header.
createdAtstring
When the event happened (ISO 8601).
dataobject
The event's payload, below.

Events#

Events fire for drops you own, and for drops sent as a team when the webhook was created with that team's organizationId.

EventWhendata
drop.createdA drop is created (before its files finish uploading).{ id, url, title, files }
drop.readyEvery file of a new drop has uploaded and the link is live.{ id, url }
drop.viewedSomeone other than you opens the drop. Repeat visits within 30 minutes count as one view.{ id, country, device, viewer }
drop.downloadedSomeone other than you downloads a file. “Download all” sends one event.{ dropId, fileId }
drop.expiredThe drop expired, or a burn-after-reading drop was opened and removed.{ id, reason }
comment.createdA comment or reply is posted.{ dropId, commentId, author, content, anchor }
approval.createdSomeone approves or requests changes.{ dropId, id, versionId, fileId, decision, signerName, signerEmail, signerVerified, note, createdAt }
version.createdA new version goes live behind the same link.{ dropId, versionId, number, note }
request.fulfilledSomeone uploads to one of your file requests.{ requestId, dropId, uploader, itemId }

Payload details#

  • drop.viewed: country is a two-letter code or null; device is "Desktop", "Mobile" or "Tablet"; viewer is the viewer's email when the drop is private and they're signed in, otherwise null (viewers of public and passcode drops aren't identified).
  • drop.expired: reason is "expired" or "burned".
  • comment.created: anchor says where the comment sits, for example { "type": "pdf_pin", "page": 3, "x": 0.4, "y": 0.2 }, { "type": "video_timestamp", "t": 42 } or { "type": "file" } for a comment on the whole file. On Vault drops content is ciphertext.
  • approval.created: decision is "approved" or "changes_requested". fileId is set when the sign-off was for one file, and signerVerified is true when the signer was signed in with a verified email.
  • drop.ready fires once, for the first version. Later uploads send version.created.

Verify the signature#

Each delivery has a Slingdrop-Signature header like t=1767225600,v1=5f2b…. v1 is the hex HMAC-SHA256 of `${t}.${body}` (the timestamp, a dot, then the raw request body) keyed with your webhook secret. Compute it over the raw body, before parsing JSON, compare in constant time, and reject old timestamps so a captured request can't be replayed.

app/api/slingdrop/route.ts
import { verifyWebhook } from "@slingdrop/sdk";

export async function POST(req: Request) {
  const body = await req.text(); // the raw body, not parsed JSON
  const ok = await verifyWebhook(process.env.SLINGDROP_WEBHOOK_SECRET!, req.headers.get("slingdrop-signature"), body);
  if (!ok) return new Response("bad signature", { status: 401 });

  const event = JSON.parse(body) as { id: string; type: string; createdAt: string; data: Record<string, unknown> };
  if (event.type === "approval.created" && event.data.decision === "approved") {
    // ship it
  }
  return new Response("ok");
}

verifyWebhook(secret, signature, body, toleranceSec = 300) from @slingdrop/sdk uses Web Crypto, so it runs in Node 20+, Workers, Deno, Bun and edge runtimes.

Retries and timeouts#

Answer with any 2xx status within 10 seconds. Anything else (a non-2xx status, a timeout, a connection error) is retried with growing delays: 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, 6 hours, 6 hours and 4 hours, about 24 hours in all. Every attempt shows up under GET /webhooks/{id}/deliveries.

Respond first, work later

Return 200 as soon as the signature checks out and do slow work in the background. Deliveries can arrive more than once and out of order, so key your handling on the event id.