Event listeners watch a server for file activity. When an event matches, we send a webhook to your URL or an email to your address.
Create a listener
Open a server, go to Event listeners and click Add listener:
- Pick the events to trigger on.
- Optionally add filters. All filters must match.
- Choose Webhook and enter an
https://URL, or choose Email and enter an address.
For webhooks, a signing secret starting with whsec_ is shown once. Store it with your application.
Use Send test to deliver a sample event right away.
Events
| Event | When |
|---|---|
upload |
A file was uploaded |
first-upload |
A file was uploaded for the first time in a session |
download |
A file was downloaded |
first-download |
A file was downloaded for the first time in a session |
delete |
A file was deleted |
rename |
A file or folder was renamed or moved |
mkdir |
A folder was created |
rmdir |
A folder was removed |
ssh_cmd |
An SSH command such as md5sum was run |
Filters
Filter on the file path or the username with contains, does not contain, starts with, ends with or equals. For example, path starts with /incoming only fires for files dropped into that folder.
Payload
Webhooks are POST requests with a JSON body:
{
"user": { "id": "acme", "name": "acme" },
"event": "upload",
"status": "OK",
"full_path": "/incoming/invoices.zip",
"protocol": "SFTP",
"ip": "203.0.113.10",
"timestamp": 1792575945021000000,
"file_size": 48213,
"download_url": "https://nbg1.your-objectstorage.com/…?X-Amz-Signature=…",
"download_expires_at": 1792579545
}
| Field | Description |
|---|---|
event |
One of the events above. |
status |
OK, or KO when the operation failed. |
full_path |
Path of the file. For rename, the new path. |
source_path |
Only for rename: the previous path. |
timestamp |
Unix time in nanoseconds. |
download_url |
A link to download the file, valid for one hour. Sent for successful upload, rename and download events. |
download_expires_at |
Unix time in seconds when download_url stops working. |
Each delivery attempt carries a fresh download_url, so retried webhooks always have a working link. Fetch the file with a plain GET; no credentials are needed.
Temporary file names
Many FTP clients upload under a temporary name and rename the file when the transfer completes, for example offers.zip.part → offers.zip. To process only finished files, listen to both upload and rename and add a filter such as path ends with .zip. Because full_path holds the new name for renames, the same handler works for both events.
Verifying signatures
Each request has an X-CloudFile-Signature header like t=1792575945,v1=5f2c…. Compute an HMAC-SHA256 of {t}.{raw body} with your signing secret and compare it to v1. Reject requests where t is more than five minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(expected, parts["v1"])
Retries
Respond with any 2xx status within 10 seconds. Otherwise we retry with exponential backoff for several hours. Each attempt is listed under the listener with its status code or error.
Webhook URLs must use HTTPS and resolve to a public address.