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
Event When 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
{
"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: ..."
}
}
Header Example Content-Typeapplication/jsonUser-AgentPhalaCloud-Webhook/1.0X-Webhook-Idevt_a1b2c3d4e5f67890X-Webhook-Eventcvm.createdX-Webhook-Timestamp1679012345X-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}")
Using SDKs (Recommended)
The Phala Cloud SDKs provide built-in verification with timestamp tolerance checks and constant-time comparison.
JavaScript / TypeScript
Python
Go
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.
Manual Verification (Python / Node.js)
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
Aspect Behavior Success HTTP 2xx (200, 201, 202, etc.) Failure Any non-2xx status, network error, or timeout Redirects Not followed (3xx = failure) Timeout 10 seconds Retries Up to 3 retries: 1 min → 10 min → 1 hour Total attempts 4 (1 initial + 3 retries) Recording One 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 localhost — localhost 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