Webhooks

DevStorage POSTs a signed event to your endpoint whenever an upload completes — regardless of which SDK, or whether the uploading browser stuck around. Treat webhooks as the source of truth for billing and records; client-side completion callbacks are for UX.

Setup

In your project's Settings, set a webhook URL and copy the signing secret. The endpoint must be publicly reachable and respond with a 2xx within 10 seconds — do slow work after responding.

Events

EventFires when
upload.completedA file finished uploading and was verified — via any SDK, the REST API, or a multipart flow. data is the file record, including any metadata you attached at presign time.
payload
{
  "id": "whd_01J...",
  "event": "upload.completed",
  "createdAt": "2026-07-02T12:35:10.000Z",
  "data": {
    "id": "file_01J...",
    "name": "photo.jpg",
    "mimeType": "image/jpeg",
    "sizeBytes": 482113,
    "r2Key": "proj_x/file_01J.../photo.jpg",
    "url": "https://...",
    "metadata": { "userId": "u42" }
  }
}

Verify signatures

Every delivery is signed with HMAC-SHA256 over the exact request body, hex-encoded in the X-DevStorage-Signature header. Always verify before trusting a payload.

Next.js

// app/api/webhooks/devstorage/route.ts
import { verifyWebhook } from "@devstorage/next/server";

export async function POST(request: Request) {
  const event = await verifyWebhook(
    request,
    process.env.DEVSTORAGE_WEBHOOK_SECRET!,
  );

  if (event.event === "upload.completed") {
    await recordFile(event.data);   // idempotent — retries reuse event.id
  }
  return new Response("ok");
}

Any Node.js server

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, signature: string, secret: string) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  return (
    expected.length === signature.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
  );
}

// Express example — keep the raw body for the HMAC
app.post("/webhooks/devstorage", express.text({ type: "*/*" }), (req, res) => {
  if (!verify(req.body, req.headers["x-devstorage-signature"], SECRET)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body);
  // ...
  res.status(200).end();
});
Compute the HMAC over the raw body string. Parsing the JSON and re-serializing it can reorder keys or change whitespace, which breaks the signature.

Retries & idempotency

A delivery that fails (non-2xx, timeout, or connection error) is retried after 1 minute, 10 minutes, and 1 hour — four attempts in total. Every retry reuses the same body and the same id (also sent as the X-DevStorage-Delivery header), so deduplicate on it:

HeaderDescription
X-DevStorage-SignatureHex HMAC-SHA256 of the body, keyed with your webhook secret.
X-DevStorage-EventThe event name, e.g. upload.completed.
X-DevStorage-DeliveryUnique delivery id — stable across retries, use it to dedupe.

Rotating the secret in your dashboard takes effect on the next delivery attempt, including pending retries.