Webhooks
When a file is uploaded to a directory that has a webhook URL configured, Bridglet sends an HTTP POST to that URL with a JSON payload describing the upload. No polling required.
Setting a webhook URL
Set the webhook URL on the directory page in your dashboard, or via the API with PATCH /api/v1/directories/:id (see the API reference). The URL must start with https:// or http://. Directories without a webhook URL simply skip notifications.
Payload
Each delivery is a POST with Content-Type: application/json and this body:
{
"event": "file.uploaded",
"directory_id": "a1b2c3d4-...",
"external_id": "cust_12345",
"filename": "report_2026-03-28.csv",
"size_bytes": 48291,
"uploaded_at": "2026-03-28T14:22:31.000000Z",
"directory": "acme-exports"
}
| event | Always "file.uploaded" today. |
| directory_id | UUID of the directory. |
| external_id | The external ID you set on the directory (your own reference), or null. |
| filename | Name of the uploaded file. |
| size_bytes | File size in bytes. |
| uploaded_at | ISO 8601 UTC timestamp. |
| directory | The directory's slug. |
Verifying the signature
Every delivery includes this header:
x-bridglet-signature: sha256=7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069
The value after sha256= is the lowercase hex HMAC-SHA256 of the raw request body, keyed with the directory's webhook secret. Every directory gets its own secret, generated automatically when the directory is created. You can read it as the webhook_secret field in any API response for the directory (for example GET /api/v1/directories/:id) or copy it from the directory page in your dashboard.
Two rules for correct verification: compute the HMAC over the raw, unparsed request body (not re-serialized JSON), and compare signatures with a constant-time comparison to avoid timing attacks. The samples below do both.
Node.js (Express)
const crypto = require("crypto");
// Use the RAW body for this route, not parsed JSON.
app.post(
"/bridglet-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const secret = process.env.BRIDGLET_WEBHOOK_SECRET;
const signature = req.get("x-bridglet-signature") || "";
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(req.body).digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body);
console.log("file uploaded:", event.filename);
res.status(200).send("ok");
}
);
Python (Flask)
import hashlib
import hmac
import os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/bridglet-webhook")
def bridglet_webhook():
secret = os.environ["BRIDGLET_WEBHOOK_SECRET"].encode()
signature = request.headers.get("x-bridglet-signature", "")
mac = hmac.new(secret, request.get_data(), hashlib.sha256)
expected = "sha256=" + mac.hexdigest()
# hmac.compare_digest is a constant-time comparison
if not hmac.compare_digest(expected, signature):
abort(401)
event = request.get_json()
print("file uploaded:", event["filename"])
return "ok", 200
Elixir (Phoenix)
defmodule MyAppWeb.BridgletWebhookController do
use MyAppWeb, :controller
# raw_body must be the unparsed request body. Capture it with a
# custom :body_reader on Plug.Parsers, then read it here from
# conn.assigns (or conn.private), depending on where you stored it.
def create(conn, _params) do
secret = System.fetch_env!("BRIDGLET_WEBHOOK_SECRET")
[signature] = get_req_header(conn, "x-bridglet-signature")
raw_body = conn.assigns.raw_body
expected =
:crypto.mac(:hmac, :sha256, secret, raw_body)
|> Base.encode16(case: :lower)
# Plug.Crypto.secure_compare/2 is a constant-time comparison
if Plug.Crypto.secure_compare("sha256=#{expected}", signature) do
send_resp(conn, 200, "ok")
else
send_resp(conn, 401, "invalid signature")
end
end
end
Delivery and retries
- Your endpoint must respond with a 2xx status within 5 seconds. Any other status, or a timeout, counts as a failed delivery.
- Failed deliveries are retried up to 3 times (4 attempts total) with exponential backoff between attempts.
- Respond fast: acknowledge with 200 first and do heavy processing (downloading the file, database writes) asynchronously.
- Deliveries can occasionally arrive more than once — make your handler idempotent, for example by keying on directory_id + filename + uploaded_at.