Webhooks
Webhooks push resource lifecycle events to an HTTPS endpoint you control. They are driven by the platform's internal event pipeline, not by your API calls, so they fire whether a change originated from the API, the web app or the mobile app.
Registering an endpoint
curl -X POST https://api.filedgr.network/notifications/webhooks \
-H "x-api-key: $FILEDGR_API_KEY" \
-H "x-api-secret: $FILEDGR_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/filedgr",
"event_types": ["vault.created", "vault.completed", "signature.completed"],
"description": "Production listener",
"secret": "a-long-random-string"
}'
| Field | Type | Required | Notes |
|---|---|---|---|
url | string | yes | See destination rules |
event_types | string[] | yes | Non-empty; each must be a known event type. No wildcards |
description | string | no | Free text |
secret | string | no | HMAC signing key. Strongly recommended |
custom_headers | object | no | Merged into the request headers, and may override the standard ones |
resource_filters | object | no | See filters |
Manage them with GET/PUT/DELETE /notifications/webhooks/{config_id}, and inspect attempts with
GET /notifications/webhooks/{config_id}/deliveries.
secret is stored and returned in plaintext by GET /notifications/webhooks. Anyone who can read
your webhook configuration can forge signed deliveries. Scope your API credentials accordingly.
Event types
| Resource | Events |
|---|---|
| Vault | vault.created vault.completed vault.failed vault.error vault.deleted |
| Stream | stream.created stream.completed stream.failed stream.error stream.deleted |
| Data attachment | data_attachment.created data_attachment.completed data_attachment.failed data_attachment.deleted |
| Signature | signature.created signature.completed signature.failed signature.error |
| Asset | asset.created asset.deployed asset.error |
| Vault permission | vault.permission.granted vault.permission.revoked vault.permission.updated |
| Stream permission | stream.permission.granted stream.permission.revoked stream.permission.updated |
Registration also accepts vault.updated, stream.updated, data_attachment.updated,
data_attachment.received, signature.updated and asset.updated. These are currently never
emitted — subscribing to them is accepted but will not deliver anything. Build against the table
above.
Events are derived from resource status transitions. Intermediate pipeline states produce no event,
so you will see a *.created and later a *.completed, not a message per internal step.
Delivery payload
{
"event_type": "vault.created",
"timestamp": "2026-08-26T12:00:00.123456",
"data": { "id": "...", "name": "...", "status": "FILEDGR_RECEIVED" }
}
data is the full resource payload for the event. timestamp is UTC but is naive — it carries
no Z suffix and no offset. Parse it as UTC explicitly.
Request headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Filedgr-Webhook/1.0 |
X-Filedgr-Event | The event type |
X-Filedgr-Signature | sha256=<hex> — only present when secret is set |
There is no delivery id and no timestamp header, so signatures are not replay-protected.
Deduplicate on data.id combined with event_type, and reject events you have already processed.
Verifying signatures
The signature is an HMAC-SHA256 over the raw request body, keyed with your secret.
import hmac, hashlib
def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
const crypto = require("crypto");
function isValid(rawBody, header, secret) {
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
Hash the raw bytes exactly as received. Re-serialising the parsed JSON will change key order and whitespace, and the signature will not match.
Retries and failure handling
- Up to 3 attempts per event, with a 30 second timeout each.
- Backoff between attempts is 2s then 4s.
- A
4xxother than429aborts the inline retry loop after the first attempt. 5xx,429, timeouts and connection errors consume all three attempts.
A background job also re-sends deliveries that are still failed, were attempted within the last 24 hours, and have recorded fewer than 3 attempts.
Because a 4xx abort records only one attempt, that event is picked up by the background job and
re-sent over the following minutes. A 4xx delays delivery rather than permanently discarding it,
so make your handler idempotent.
Return a 2xx as soon as you have durably accepted the event, and process it asynchronously — anything slower than 30 seconds counts as a failure.
Auto-disable
Each failed delivery increments a counter; each success resets it to zero. At 10 consecutive
failures the webhook's status flips to DISABLED and deliveries stop. Re-enable it with:
curl -X PUT https://api.filedgr.network/notifications/webhooks/$CONFIG_ID \
-H "x-api-key: $FILEDGR_API_KEY" -H "x-api-secret: $FILEDGR_API_SECRET" \
-H "Content-Type: application/json" -d '{"status": "ACTIVE"}'
Status values are ACTIVE, PAUSED and DISABLED. Only ACTIVE webhooks receive deliveries.
Deduplication window
Identical events — same entity, same resource, same event type — are suppressed if one was already recorded within the previous 5 minutes. The check runs before every channel, so it affects webhooks too.
The deduplication record lives in the in-app notification table. If in-app notifications are disabled by preferences or hit their rate limit, no record is written and nothing is suppressed — so deduplication is not something you can rely on. Deduplicate on your side regardless.
Destination rules
Delivery targets are validated before any request is made. A URL is rejected outright if:
- the scheme is not
httporhttps; - the port is not 80 or 443;
- the hostname resolves to a private, loopback, link-local, reserved, multicast or unspecified address.
Blocked attempts appear in delivery history with no status code and the message
blocked: destination not allowed.
This is the usual reason a webhook "never fires" during local testing — tunnels pointing at localhost and private addresses are refused. Test against a publicly resolvable host.
Use HTTPS in production. Plain http on port 80 is accepted, but your payloads and signatures would
travel in the clear.
Resource filters
Restrict a webhook to specific resources:
{ "resource_filters": { "vault_ids": ["<uuid>"], "stream_ids": ["<uuid>"] } }
Filters only understand vault_ids and stream_ids. If resource_filters is set, events for any
other resource type — data_attachment.*, signature.*, asset.* — match nothing and are
silently dropped. Register a second, unfiltered webhook for those event types.
Webhooks ignore notification preferences
Notification preferences govern the email, in-app and push channels. Webhook delivery is never gated by them — disabling all notifications does not stop webhooks. Delete or disable the webhook configuration itself.