Webhooks
Webhooks let you receive HTTP notifications when events occur on your FoundryDB services. Instead of polling the API, you register an HTTPS endpoint and FoundryDB posts a signed JSON payload to it whenever a matching event fires.
Overview
Each webhook endpoint you register specifies:
- A target URL that receives
POSTrequests. - An optional list of event types to filter. If omitted, all events are delivered.
- A signing secret used to verify that requests genuinely come from FoundryDB.
Delivery is asynchronous and non-blocking. The service state machine is never held waiting for your endpoint to respond.
Creating a webhook endpoint
curl -u admin:password -X POST https://api.foundrydb.com/webhooks \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/hooks/foundrydb",
"events": ["service.running", "backup.completed", "backup.failed"]
}'
To subscribe to all events, omit the events field entirely.
Response (201 Created):
{
"id": "a3f8c1d2-4e77-4b2a-9f0e-1234567890ab",
"url": "https://your-app.example.com/hooks/foundrydb",
"events": ["service.running", "backup.completed", "backup.failed"],
"active": true,
"secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"created_at": "2026-03-31T10:00:00Z",
"updated_at": "2026-03-31T10:00:00Z"
}
The secret is returned only at creation time. Store it securely; it cannot be retrieved again. If it is lost, delete the endpoint and create a new one.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Target URL. Must begin with http:// or https://. |
events | string[] | No | Event types to deliver. Omit to receive all events. |
Event types
| Event type | Fired when |
|---|---|
service.running | A service finishes provisioning and is ready to accept connections. |
service.error | A service enters an error state (provisioning failed, health check failed). |
service.stopped | A service is stopped. |
service.deleted | A service is fully deleted and all resources released. |
backup.completed | A scheduled or on-demand backup finishes successfully. |
backup.failed | A backup attempt fails. |
alert.fired | A monitoring alert threshold is breached. |
alert.resolved | A previously fired alert returns to a normal state. |
webhook.test | Sent when you trigger a test delivery via the API. |
Payload structure
Every event is delivered as a POST request with Content-Type: application/json. The body follows a consistent envelope:
{
"id": "e9c1a2b3-0000-4f5a-8c7d-abcdef012345",
"event_type": "service.running",
"timestamp": "2026-03-31T10:15:42.318Z",
"data": {
"service_id": "svc_7f3d9a1b2c4e5f6a",
"service_name": "prod-postgres",
"database_type": "postgresql",
"status": "running"
}
}
Envelope fields
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier for this event. Use for deduplication. |
event_type | string | One of the event types listed above. |
timestamp | string (RFC 3339) | UTC time when the event was generated. |
data | object | Event-specific payload. Contents vary by event type (see below). |
Example: backup.completed
{
"id": "f1a2b3c4-1111-4d5e-9f0a-112233445566",
"event_type": "backup.completed",
"timestamp": "2026-03-31T02:00:18.004Z",
"data": {
"service_id": "svc_7f3d9a1b2c4e5f6a",
"backup_id": "bkp_5e6f7a8b9c0d1e2f",
"backup_type": "full",
"size_bytes": 2147483648,
"duration_seconds": 42
}
}
Example: alert.fired
{
"id": "d4e5f6a7-2222-4b8c-a9d0-aabbccddeeff",
"event_type": "alert.fired",
"timestamp": "2026-03-31T14:33:07.891Z",
"data": {
"service_id": "svc_7f3d9a1b2c4e5f6a",
"alert_name": "high_cpu",
"metric": "cpu",
"threshold": 90,
"current_value": 97.2
}
}
Signature verification
Every request includes an X-FoundryDB-Signature header containing an HMAC-SHA256 signature of the raw request body. Verify this before processing the payload.
The signature format is:
X-FoundryDB-Signature: sha256=<hex-encoded-digest>
The digest is computed as HMAC-SHA256(secret, raw_request_body).
Always verify the signature using the raw bytes of the request body before JSON-parsing it.
Python
import hashlib
import hmac
def verify_signature(payload_bytes: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
payload_bytes,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, header)
# In your request handler:
raw_body = request.get_data() # bytes, before any parsing
sig_header = request.headers.get("X-FoundryDB-Signature", "")
if not verify_signature(raw_body, sig_header, WEBHOOK_SECRET):
return "Unauthorized", 401
Node.js
const crypto = require("crypto");
function verifySignature(rawBody, header, secret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "utf8"),
Buffer.from(header, "utf8"),
);
}
// In your Express handler:
app.post("/hooks/foundrydb", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-foundrydb-signature"] || "";
if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Unauthorized");
}
const event = JSON.parse(req.body.toString("utf8"));
// handle event...
res.status(200).send("OK");
});
Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
func verifySignature(payload []byte, header, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(header))
}
// In your HTTP handler:
func handleWebhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-FoundryDB-Signature")
if !verifySignature(body, sig, webhookSecret) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
var event WebhookEventPayload
json.Unmarshal(body, &event)
// handle event...
w.WriteHeader(http.StatusOK)
}
Delivery and retries
FoundryDB delivers events asynchronously, independent of the action that triggered them.
Retry policy
| Attempt | Delay before attempt |
|---|---|
| 1 (initial) | Immediate |
| 2 | 1 second |
| 3 | 2 seconds |
After 3 failed attempts the delivery is marked as permanently failed and no further retries occur.
A delivery is considered successful when your endpoint responds with any 2xx status code within 10 seconds. Non-2xx responses and connection timeouts are treated as failures.
What counts as a failure
- HTTP response status outside the
200-299range. - Connection timeout (10 second limit per attempt).
- Connection refused or DNS resolution failure.
- TLS handshake error.
Delivery records
Every delivery attempt, whether successful or not, is recorded. You can inspect recent delivery history per endpoint:
curl -u admin:password https://api.foundrydb.com/webhooks/{webhook-id}/deliveries \
Each record includes the attempt number, HTTP response status, error message (if any), and timestamps.
Endpoint behaviour recommendations
- Respond quickly. Acknowledge the request with
200 OKimmediately and process the payload asynchronously. Avoid doing heavy work inside the HTTP handler, as the 10-second timeout applies to the full round-trip. - Deduplicate by event ID. Network conditions can cause the same event to be delivered more than once. Use the
idfield to detect and discard duplicates. - Return
200even for events you ignore. If your handler does not care about a particular event type, still respond with200. Returning a non-2xx status will cause FoundryDB to retry that delivery unnecessarily.
Testing
Send a test event to verify that your endpoint is reachable and your signature verification logic is correct:
curl -u admin:password -X POST https://api.foundrydb.com/webhooks/{webhook-id}/test \
Response (202 Accepted):
{
"message": "Test event dispatched"
}
This delivers a webhook.test event to your endpoint with the following data payload:
{
"webhook_id": "a3f8c1d2-4e77-4b2a-9f0e-1234567890ab",
"message": "This is a test event from FoundryDB."
}
The test event goes through the same signing and retry logic as live events, so it is a faithful end-to-end test of the delivery path.
Managing webhooks
List all endpoints
curl -u admin:password https://api.foundrydb.com/webhooks \
{
"webhooks": [
{
"id": "a3f8c1d2-4e77-4b2a-9f0e-1234567890ab",
"url": "https://your-app.example.com/hooks/foundrydb",
"events": ["service.running", "backup.completed", "backup.failed"],
"active": true,
"created_at": "2026-03-31T10:00:00Z",
"updated_at": "2026-03-31T10:00:00Z"
}
]
}
Get a single endpoint
curl -u admin:password https://api.foundrydb.com/webhooks/{webhook-id} \
Delete an endpoint
Deleting an endpoint stops all future deliveries. In-flight deliveries already queued may still complete.
curl -u admin:password -X DELETE https://api.foundrydb.com/webhooks/{webhook-id} \
Returns 204 No Content on success.
List recent deliveries
curl -u admin:password https://api.foundrydb.com/webhooks/{webhook-id}/deliveries \
Returns up to 100 of the most recent delivery records, newest first.
{
"deliveries": [
{
"id": "d1e2f3a4-...",
"webhook_id": "a3f8c1d2-...",
"event_type": "backup.completed",
"response_status": 200,
"delivered_at": "2026-03-31T02:00:18Z",
"created_at": "2026-03-31T02:00:17Z"
}
]
}
API reference summary
| Method | Path | Description |
|---|---|---|
POST | /webhooks | Create a webhook endpoint |
GET | /webhooks | List all webhook endpoints |
GET | /webhooks/{id} | Get a single endpoint |
DELETE | /webhooks/{id} | Delete an endpoint |
POST | /webhooks/{id}/test | Send a test event |
GET | /webhooks/{id}/deliveries | List delivery history |