Skip to main content
Phala Cloud sends HTTP POST requests to your endpoint when events occur in your workspace — CVM deployments, updates, stops, deletes, and multisig approval requests. Each request includes an HMAC-SHA256 signature for verification.

Supported Events

EventWhen
cvm.createdCVM first deployment completed, now running
cvm.startedExisting CVM started (after stop)
cvm.create_failedCVM deployment failed (boot error, timeout)
cvm.restartedCVM restart completed
cvm.updatedCVM compose update completed, now running
cvm.stoppedCVM stopped or shut down
cvm.deletedCVM deleted
cvm.update.pending_approvalOn-chain KMS update requires multisig approval

Payload Format

{
  "id": "evt_a1b2c3d4e5f67890",
  "event": "cvm.created",
  "version": "1",
  "created_at": "2026-03-19T10:30:00Z",
  "workspace": {
    "id": "my-team-slug",
    "name": "My Team"
  },
  "data": {
    "cvm_id": "1a09d706-2686-4e0a-8b1d-323ff0e3504b",
    "cvm_name": "my-app",
    "app_id": "0xabc...",
    "status": "running"
  }
}
For failure events, data includes an error field:
{
  "data": {
    "cvm_id": "...",
    "cvm_name": "my-app",
    "app_id": "0xabc...",
    "status": "error",
    "error": "CVM 82: Failed to request app keys: ..."
  }
}

HTTP Headers

HeaderExample
Content-Typeapplication/json
User-AgentPhalaCloud-Webhook/1.0
X-Webhook-Idevt_a1b2c3d4e5f67890
X-Webhook-Eventcvm.created
X-Webhook-Timestamp1679012345
X-Webhook-Signaturesha256=a1b2c3...

Verifying Signatures

Each webhook has a signing secret (starts with whsec_). Use it to verify requests are authentic and untampered. The signature is: sha256=HMAC-SHA256(secret, "{timestamp}.{raw_body}") The Phala Cloud SDKs provide built-in verification with timestamp tolerance checks and constant-time comparison.
import { verifyWebhookSignature, parseWebhookEvent } from "@phala/cloud/webhook";

// Option 1: Verify signature manually
const isValid = verifyWebhookSignature({
  secret: "whsec_...",
  timestamp: req.headers["x-webhook-timestamp"],
  body: rawBody,
  signature: req.headers["x-webhook-signature"],
});

// Option 2: Parse and verify in one step (throws on failure)
try {
  const event = parseWebhookEvent({
    headers: req.headers,
    body: rawBody,
    secret: "whsec_...",
  });
  console.log(event.event, event.data);
} catch (err) {
  // Invalid signature or expired timestamp
  res.status(401).send("Invalid signature");
}
All three SDKs automatically reject timestamps older than 5 minutes (configurable via toleranceSeconds) and use constant-time comparison to prevent timing attacks.
import hmac
import hashlib

def verify_webhook(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    message = f"{timestamp}.{body.decode('utf-8')}".encode("utf-8")
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"), message, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
Replay protection: reject events where X-Webhook-Timestamp is older than 5 minutes.

Delivery Behavior

AspectBehavior
SuccessHTTP 2xx (200, 201, 202, etc.)
FailureAny non-2xx status, network error, or timeout
RedirectsNot followed (3xx = failure)
Timeout10 seconds
RetriesUp to 3 retries: 1 min → 10 min → 1 hour
Total attempts4 (1 initial + 3 retries)
RecordingOne delivery record per event (final result only)

Secret Management

Webhook signing secrets follow a strict security model:
  • Shown once at creation — the full secret is returned only in the POST /workspace/webhooks response
  • Masked afterward — subsequent GET/LIST responses return a masked value (e.g. whsec_****...xxxx)
  • Reveal requires 2FA — to view the full secret again, your account must have two-factor authentication enabled and recently verified
  • Rotation requires 2FA — generating a new secret also requires 2FA verification

Reveal Secret

POST /api/v1/workspace/webhooks/{id}/reveal-secret
Returns the full signing secret. Requires:
  • Account with 2FA enabled (returns 403 if not)
  • Recent 2FA step-up verification (returns 428 if not verified)

Rotate Secret

POST /api/v1/workspace/webhooks/{id}/rotate-secret
Generates a new signing secret and invalidates the previous one. Same 2FA requirements as reveal.
After rotation, update your webhook consumer with the new secret immediately. Events signed with the old secret will fail verification.

URL Requirements

Webhook URLs must meet the following requirements:
  • HTTPS only — HTTP URLs are rejected
  • Public addresses only — URLs resolving to private or reserved IP ranges are blocked:
    • 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (private)
    • 169.254.0.0/16 including 169.254.169.254 (cloud metadata)
    • ::1, fc00::/7, fe80::/10 (IPv6 private/loopback)
  • No localhostlocalhost and .local domains are not allowed
These restrictions apply both at webhook creation and at delivery time to prevent DNS rebinding attacks.

Managing Webhooks

Dashboard

Settings → Webhooks in your workspace:
  • Create, edit, enable/disable, delete webhooks
  • Select subscribed events
  • Send test events
  • View delivery history with stats (success rate, response time)
  • Resend failed deliveries

API

# Create
POST /api/v1/workspace/webhooks
{"url": "https://example.com/webhook", "events": ["cvm.created", "cvm.stopped"], "name": "My Hook"}

# List
GET /api/v1/workspace/webhooks

# Update
PUT /api/v1/workspace/webhooks/{id}
{"enabled": false}

# Delete
DELETE /api/v1/workspace/webhooks/{id}

# Test
POST /api/v1/workspace/webhooks/{id}/test

# Deliveries
GET /api/v1/workspace/webhooks/{id}/deliveries

# Stats
GET /api/v1/workspace/webhooks/{id}/stats

# Resend
POST /api/v1/workspace/webhooks/{id}/deliveries/{event_id}/resend

Best Practices

  • Verify signatures — always check X-Webhook-Signature before processing
  • Respond fast — return 200 immediately, process asynchronously (10s timeout)
  • Be idempotent — use the id field to deduplicate retries
  • Check timestamps — reject events older than 5 minutes