Skip to content

Webhooks

dropkit sends signed webhook POSTs for two kinds of events. File events fire on upload and delete within a project. Billing events fire when your card is declined, when an overage is reported to Polar, or when your plan changes.

Configure each webhook separately. Both use the same signing contract.

Signing

Every request includes:

content-type: application/json
dropkit-event: <event name>
dropkit-signature: sha256=<hex>
dropkit-delivery: evt_<hex>
user-agent: dropkit-webhooks/1

The signature is HMAC_SHA256(secret, rawBody) hex-encoded. Verify on your end:

import crypto from 'node:crypto';
const sig = req.headers['dropkit-signature'].replace('sha256=', '');
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}

Rotate the secret any time from the dashboard. Your server starts rejecting events until you update it on your end.

File events (per project)

Configure at /[slug]/webhooks on the dashboard. One URL per project. Different projects can have different webhook URLs.

Events:

  • file.uploaded fires after a successful REST or SDK upload. Payload includes file.id, file.name, file.mime, file.size, file.url.
  • file.deleted fires when a file is removed via the API. Payload includes the same fields.

Example payload:

{
"id": "evt_7c5a1b3f...",
"event": "file.uploaded",
"createdAt": 1800000000000,
"projectId": "proj_...",
"file": {
"id": "aB3cD4eF5gH6",
"name": "photo.jpg",
"mime": "image/jpeg",
"size": 248193,
"url": "https://cdn.dropkit.app/aB3cD4eF5gH6"
}
}

dropkit also auto-formats the payload for Discord and Slack webhook URLs. Drop in a Discord or Slack URL and you get embeds or blocks directly.

Billing events (per account)

Configure at /billing on the dashboard. One URL per account, not per project.

Events:

  • billing.card_declined: your Polar subscription went past due. Uploads are paused until the card is updated. Data: { plan, polar_status }.
  • billing.card_recovered: past-due resolved. Uploads resume. Data: { plan, polar_status }.
  • billing.overage_reported: nightly cron reported your storage overage to Polar for the current billing period. Fires once per day per user while they are over. Data: { plan, gb, used_bytes, cap_bytes }.
  • billing.plan_changed: your subscription was canceled or moved to a different plan. Data: { new_plan, reason }.

Example payload:

{
"id": "evt_a9b2c1...",
"event": "billing.overage_reported",
"createdAt": 1800000000000,
"userId": "user_...",
"data": {
"plan": "hobby",
"gb": 47,
"used_bytes": 158461329408,
"cap_bytes": 107374182400
}
}

Use these to wire up your own Slack alert, PagerDuty page, or internal dashboard. dropkit intentionally does not send billing emails; the webhook is the integration point.

Storage caps and overage

dropkit charges $0.12 per GB per month past your plan’s included storage. Current caps:

planincluded storageper-file cap
Free5 GB (hard cap; uploads fail past this)100 MB
Hobby100 GB5 GB
Pro500 GB5 GB
Scale2 TB5 GB

Paid plans do not block uploads when you exceed the cap. Instead, a nightly cron computes your overage and reports it to Polar via /events/ingest. Polar bills the peak overage per billing period (max aggregation). You get a billing.overage_reported webhook each night while over cap.

If your card is declined, uploads pause and you get billing.card_declined. Fix it in the Polar portal from the dashboard billing page and uploads resume along with a billing.card_recovered event.

Retries and reliability

Webhooks are best-effort. We do not retry failed deliveries. Your endpoint should:

  • Respond with 200 as fast as possible. Do the heavy work async.
  • Accept duplicates. We include a dropkit-delivery: evt_<hex> header you can use for idempotency.
  • Timeout gracefully on your end; we cap requests at 10 seconds.

If you need guaranteed delivery, poll the API for state. Webhooks are a push optimization, not the source of truth.