Webhooks
Lockwell can POST a signed event notification to your endpoint whenever an object is created or removed in a bucket. You configure the target with the native client, and you verify each delivery with a constant-time HMAC in one call.
Webhook is the only delivery target. SNS, SQS, and Lambda destinations are a deliberate non-goal (the server returns 501). There is one HTTP(S) target per configuration.
Keep credentials out of the target URL Query strings are accepted, but a failed delivery currently records
the full target URL in the job's LastError and append-only audit reason, and operator job output can print it. Use a credential-free HTTPS URL with no tokens, API keys, or secrets in its query string. Authenticate deliveries by verifying Lockwell's HMAC signature headers with the generated signing secret. :::
Notifications are a native-only surface. The S3 PutBucketNotificationConfiguration path enforces the same webhook-only rule, and there is no admin-UI control for it.
Configure a notification
setBucketNotification wires a webhook for the bucket's s3:ObjectCreated:* and s3:ObjectRemoved:* events, with optional prefix / suffix key filters. The Node helper accepts the friendly shorthand object-created / object-removed (or the canonical s3:Object* names) and the { prefix, suffix } filter object.
const created = await native.setBucketNotification("uploads", {
id: "uploads-events",
webhookUrl: "https://my-app.example.com/hooks/lockwell",
events: ["object-created", "object-removed"],
filters: { prefix: "incoming/", suffix: ".pdf" },
});
const signingSecret = created.configs[0].signingSecret; // shown once; store securely
// Read it back. The view carries `hasSecret`; signingSecret is omitted:
const { configs } = await native.getBucketNotification("uploads");
// Clear it:
await native.deleteBucketNotification("uploads");created, err := native.SetBucketNotification(ctx, "uploads", lockwellnative.SetBucketNotificationInput{
Configs: []lockwellnative.NotificationConfig{{
ID: "uploads-events",
WebhookURL: "https://my-app.example.com/hooks/lockwell",
Events: []string{"s3:ObjectCreated:*", "s3:ObjectRemoved:*"},
Filters: []lockwellnative.NotificationFilter{
{Name: "prefix", Value: "incoming/"},
{Name: "suffix", Value: ".pdf"},
},
}},
})
signingSecret := created[0].SigningSecret // shown once; store securely
// Read it back (HasSecret only; SigningSecret is empty):
views, _ := native.GetBucketNotification(ctx, "uploads")
// Clear it:
_ = native.DeleteBucketNotification(ctx, "uploads")import com.lockwell.sdk.nativeapi.NativeTypes.NotificationConfig;
var config = new NotificationConfig()
.webhookUrl("https://my-app.example.com/hooks/lockwell")
.event("s3:ObjectCreated:*")
.event("s3:ObjectRemoved:*")
.filter("prefix", "incoming/")
.filter("suffix", ".pdf");
config.id("uploads-events");
var created = nativeClient.setBucketNotification("uploads", config);
String signingSecret = created.configs().get(0).signingSecret(); // shown once
// Read it back (each config reports hasSecret(); signingSecret() is null):
var current = nativeClient.getBucketNotification("uploads");
// Clear it:
nativeClient.deleteBucketNotification("uploads");Passing an empty config list clears the bucket's notification configuration (S3 "clear" semantics). Configuring a notification needs the admin verb on the bucket, so a read-only or data-only key is denied with a 403.
How a delivery is signed
Each delivery carries two headers:
X-Lockwell-Signatureis the lowercase-hexHMAC-SHA256of the exact request body bytes.X-Lockwell-Eventis the event type (for quick routing; not part of the signature).
The payload body is S3-event-shaped: { "Records": [ { "eventName": ..., "s3": { ... } } ] }.
Verify an incoming delivery
Verify against the raw request bytes with verifyWebhook, which reproduces the server's signing exactly and compares in constant time.
Verify the exact bytes Sign the raw body you received. Do not parse and re-serialize the JSON first.
Re-encoding changes the bytes and breaks the signature, so verification fails even on a genuine delivery. Read the raw body, verify, then parse. :::
import { verifyWebhook, WEBHOOK_SIGNATURE_HEADER_NAME } from "@kelphect/sdk";
app.post("/hooks/lockwell", async (req, res) => {
const raw = await readRawBody(req); // exact bytes; do NOT JSON.parse then re-stringify
const sig = req.headers[WEBHOOK_SIGNATURE_HEADER_NAME.toLowerCase()];
const ok = await verifyWebhook(raw, sig, process.env.WEBHOOK_SECRET);
if (!ok) return res.status(401).end();
const event = JSON.parse(raw.toString("utf8"));
for (const record of event.Records ?? []) {
/* ...do real work... */
}
res.json({ verified: true });
});import "github.com/KelpHect/lockwell/pkg/lockwellkit"
func handleWebhook(w http.ResponseWriter, r *http.Request) {
raw, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // exact bytes
sig := r.Header.Get(lockwellkit.WebhookSignatureHeader)
if !lockwellkit.VerifyWebhook(raw, sig, secret) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
event := r.Header.Get(lockwellkit.WebhookEventHeader)
log.Printf("verified delivery: %s", event)
// ...do real work with the verified body...
}import com.lockwell.sdk.kit.LockwellKit;
byte[] raw = request.getInputStream().readAllBytes(); // exact bytes
String sig = request.getHeader("X-Lockwell-Signature");
if (!LockwellKit.verifyWebhook(raw, sig, secret)) {
response.setStatus(401);
return;
}
// ...do real work with the verified body...verifyWebhook is exported standalone (no kit instance needed) and is edge-safe in Node. It uses WebCrypto HMAC plus a constant-time compare with no node:crypto, so it runs unchanged on Cloudflare Workers, Vercel Edge, Bun, and Deno. See edge runtimes.
About the signing secret
Lockwell generates a 256-bit per-config secret server-side, returns it exactly once when a new config ID is created, and stores only encrypted material thereafter. Capture signingSecret from the creation response and put it in the receiver's secret manager. GET and same-ID updates expose only hasSecret; losing the value requires rotation through a new config ID. S3 PUT exposes the equivalent one-time map in X-Lockwell-Webhook-Secrets.
If your receiver has no secret to verify against, fail closed (reject the delivery) rather than trust an unverified body.
Next steps
- The app kit.
verifyWebhookis also a method onLockwellKit. - Edge runtimes. Receive and verify webhooks on a Worker.
- Data operations. Act on the object the event points to.