Skip to main content

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 POST requests.
  • 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

FieldTypeRequiredDescription
urlstringYesTarget URL. Must begin with http:// or https://.
eventsstring[]NoEvent types to deliver. Omit to receive all events.

Event types

Event typeFired when
service.runningA service finishes provisioning and is ready to accept connections.
service.errorA service enters an error state (provisioning failed, health check failed).
service.stoppedA service is stopped.
service.deletedA service is fully deleted and all resources released.
backup.completedA scheduled or on-demand backup finishes successfully.
backup.failedA backup attempt fails.
alert.firedA monitoring alert threshold is breached.
alert.resolvedA previously fired alert returns to a normal state.
webhook.testSent 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

FieldTypeDescription
idstring (UUID)Unique identifier for this event. Use for deduplication.
event_typestringOne of the event types listed above.
timestampstring (RFC 3339)UTC time when the event was generated.
dataobjectEvent-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

AttemptDelay before attempt
1 (initial)Immediate
21 second
32 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-299 range.
  • 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 OK immediately 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 id field to detect and discard duplicates.
  • Return 200 even for events you ignore. If your handler does not care about a particular event type, still respond with 200. 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

MethodPathDescription
POST/webhooksCreate a webhook endpoint
GET/webhooksList all webhook endpoints
GET/webhooks/{id}Get a single endpoint
DELETE/webhooks/{id}Delete an endpoint
POST/webhooks/{id}/testSend a test event
GET/webhooks/{id}/deliveriesList delivery history