Overview
Webhooks enable real-time notifications when events occur in your workspace. Subscribe to tunnel creation, agent connections, share access, and more.
Create Webhook
Create a new webhook subscription.
curl -X POST https://api.privateconnect.co/v1/webhooks \
-H "x-api-key: pc_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks",
"events": ["tunnel.created", "tunnel.deleted", "agent.connected"],
"description": "Production webhook"
}'
Request Body:
Webhook endpoint URL (must be HTTPS)
Webhook description (max 500 characters)
Response:
{
"id" : "wh_123" ,
"url" : "https://example.com/webhooks" ,
"events" : [ "tunnel.created" , "tunnel.deleted" , "agent.connected" ],
"secret" : "whsec_1234567890abcdefghijklmnop" ,
"description" : "Production webhook" ,
"isActive" : true ,
"createdAt" : "2026-03-02T10:00:00.000Z"
}
Save the secret value immediately - it’s only shown once and is used to verify webhook signatures.
List Webhooks
Retrieve all webhooks for the workspace.
curl https://api.privateconnect.co/v1/webhooks \
-H "x-api-key: pc_your_api_key"
Response:
[
{
"id" : "wh_123" ,
"url" : "https://example.com/webhooks" ,
"events" : [ "tunnel.created" , "tunnel.deleted" ],
"description" : "Production webhook" ,
"isActive" : true ,
"createdAt" : "2026-03-02T10:00:00.000Z" ,
"lastTriggeredAt" : "2026-03-02T11:00:00.000Z" ,
"successCount" : 42 ,
"failureCount" : 0
}
]
Get Webhook Details
Retrieve details for a specific webhook including recent delivery attempts.
curl https://api.privateconnect.co/v1/webhooks/wh_123 \
-H "x-api-key: pc_your_api_key"
Response:
{
"id" : "wh_123" ,
"url" : "https://example.com/webhooks" ,
"events" : [ "tunnel.created" , "tunnel.deleted" ],
"description" : "Production webhook" ,
"isActive" : true ,
"createdAt" : "2026-03-02T10:00:00.000Z" ,
"lastTriggeredAt" : "2026-03-02T11:00:00.000Z" ,
"successCount" : 42 ,
"failureCount" : 0 ,
"recentDeliveries" : [
{
"id" : "del_123" ,
"event" : "tunnel.created" ,
"statusCode" : 200 ,
"success" : true ,
"attemptedAt" : "2026-03-02T11:00:00.000Z" ,
"latencyMs" : 123
}
]
}
Update Webhook
Update webhook URL, events, or active status.
curl -X PATCH https://api.privateconnect.co/v1/webhooks/wh_123 \
-H "x-api-key: pc_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/v2",
"events": ["tunnel.created", "tunnel.deleted", "share.created"],
"isActive": true
}'
Request Body:
New webhook URL (must be HTTPS)
Updated array of event types
Enable or disable the webhook
Response:
{
"id" : "wh_123" ,
"url" : "https://example.com/webhooks/v2" ,
"events" : [ "tunnel.created" , "tunnel.deleted" , "share.created" ],
"isActive" : true
}
Delete Webhook
Delete a webhook and all its delivery history.
curl -X DELETE https://api.privateconnect.co/v1/webhooks/wh_123 \
-H "x-api-key: pc_your_api_key"
Response:
Rotate Webhook Secret
Generate a new HMAC secret for the webhook.
curl -X POST https://api.privateconnect.co/v1/webhooks/wh_123/rotate-secret \
-H "x-api-key: pc_your_api_key"
Response:
{
"secret" : "whsec_new_secret_here"
}
Update your webhook handler to use the new secret before the old one expires.
Test Webhook
Send a test event to the webhook endpoint.
curl -X POST https://api.privateconnect.co/v1/webhooks/wh_123/test \
-H "x-api-key: pc_your_api_key"
Response:
{
"success" : true ,
"statusCode" : 200
}
If the test fails:
{
"success" : false ,
"statusCode" : 500 ,
"error" : "Connection refused"
}
Available Events
List all available webhook event types.
curl https://api.privateconnect.co/v1/webhooks/events
Response:
{
"events" : [
{ "name" : "tunnel.created" , "description" : "A new tunnel was created" },
{ "name" : "tunnel.connected" , "description" : "An agent connected to a tunnel" },
{ "name" : "tunnel.disconnected" , "description" : "An agent disconnected from a tunnel" },
{ "name" : "tunnel.deleted" , "description" : "A tunnel was deleted" },
{ "name" : "share.created" , "description" : "A new share link was created" },
{ "name" : "share.accessed" , "description" : "A share link was accessed" },
{ "name" : "share.revoked" , "description" : "A share link was revoked" },
{ "name" : "agent.connected" , "description" : "An agent came online" },
{ "name" : "agent.disconnected" , "description" : "An agent went offline" },
{ "name" : "agent.registered" , "description" : "A new agent was registered" }
]
}
Event Types
Triggered when a new tunnel is created. Payload: {
"event" : "tunnel.created" ,
"data" : {
"id" : "tun_123" ,
"name" : "prod-db" ,
"agentId" : "550e8400-e29b-41d4-a716-446655440000" ,
"targetPort" : 5432 ,
"protocol" : "tcp" ,
"createdAt" : "2026-03-02T10:00:00.000Z"
},
"workspaceId" : "ws_123" ,
"timestamp" : "2026-03-02T10:00:00.000Z"
}
Triggered when an agent establishes a connection to a tunnel. Payload: {
"event" : "tunnel.connected" ,
"data" : {
"id" : "tun_123" ,
"agentId" : "550e8400-e29b-41d4-a716-446655440000" ,
"agentName" : "web-server-1"
},
"workspaceId" : "ws_123" ,
"timestamp" : "2026-03-02T10:00:00.000Z"
}
Triggered when an agent disconnects from a tunnel.
Triggered when a tunnel is deleted.
Triggered when a new share link is created. Payload: {
"event" : "share.created" ,
"data" : {
"id" : "share_123" ,
"name" : "contractor-access" ,
"serviceId" : "svc_123" ,
"expiresAt" : "2026-03-09T10:00:00.000Z"
},
"workspaceId" : "ws_123" ,
"timestamp" : "2026-03-02T10:00:00.000Z"
}
Triggered when a share link is accessed (rate limited to prevent spam). Payload: {
"event" : "share.accessed" ,
"data" : {
"shareId" : "share_123" ,
"ipAddress" : "203.0.113.42" ,
"path" : "/api/users" ,
"method" : "GET"
},
"workspaceId" : "ws_123" ,
"timestamp" : "2026-03-02T10:00:00.000Z"
}
Triggered when a share link is revoked.
Triggered when an agent comes online. Payload: {
"event" : "agent.connected" ,
"data" : {
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"name" : "web-server-1" ,
"label" : "production"
},
"workspaceId" : "ws_123" ,
"timestamp" : "2026-03-02T10:00:00.000Z"
}
Triggered when an agent goes offline.
Triggered when a new agent is registered.
All webhook events follow this format:
{
"id" : "evt_1234567890" ,
"event" : "tunnel.created" ,
"data" : {
// Event-specific data
},
"workspaceId" : "ws_123" ,
"timestamp" : "2026-03-02T10:00:00.000Z"
}
Workspace that triggered the event
ISO 8601 timestamp when the event occurred
Signature Verification
Webhook requests include an HMAC signature for verification:
Headers:
x-webhook-signature: sha256=abc123...
x-webhook-timestamp: 1709380800
Verify Signature (Node.js)
const crypto = require ( 'crypto' );
function verifyWebhook ( payload , signature , secret , timestamp ) {
// Prevent replay attacks (reject if older than 5 minutes)
const now = Math . floor ( Date . now () / 1000 );
if ( Math . abs ( now - timestamp ) > 300 ) {
return false ;
}
// Compute HMAC
const signedPayload = ` ${ timestamp } . ${ JSON . stringify ( payload ) } ` ;
const hmac = crypto
. createHmac ( 'sha256' , secret )
. update ( signedPayload )
. digest ( 'hex' );
// Compare signatures
return `sha256= ${ hmac } ` === signature ;
}
// Usage in Express
app . post ( '/webhooks' , ( req , res ) => {
const signature = req . headers [ 'x-webhook-signature' ];
const timestamp = req . headers [ 'x-webhook-timestamp' ];
const secret = 'whsec_your_secret' ;
if ( ! verifyWebhook ( req . body , signature , secret , timestamp )) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
// Process webhook
console . log ( 'Event:' , req . body . event );
res . status ( 200 ). send ( 'OK' );
});
Verify Signature (Python)
import hmac
import hashlib
import time
from flask import Flask, request
app = Flask( __name__ )
def verify_webhook ( payload , signature , secret , timestamp ):
# Prevent replay attacks
if abs (time.time() - int (timestamp)) > 300 :
return False
# Compute HMAC
signed_payload = f " { timestamp } . { payload } "
hmac_digest = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Compare signatures
return f "sha256= { hmac_digest } " == signature
@app.route ( '/webhooks' , methods = [ 'POST' ])
def webhook ():
signature = request.headers.get( 'x-webhook-signature' )
timestamp = request.headers.get( 'x-webhook-timestamp' )
secret = 'whsec_your_secret'
if not verify_webhook(request.data.decode(), signature, secret, timestamp):
return 'Invalid signature' , 401
# Process webhook
event = request.json
print ( f "Event: { event[ 'event' ] } " )
return 'OK' , 200
Always verify webhook signatures to prevent spoofed requests.
Delivery and Retries
Timeout : 30 seconds
Retries : 3 attempts with exponential backoff (1s, 5s, 25s)
Success : HTTP status codes 200-299
Failure : Any other status code or timeout
Response Requirements
Your webhook endpoint should:
Respond quickly (< 30 seconds)
Return HTTP 200-299 for success
Process events asynchronously if needed
Handle duplicate events (idempotency)
Example Handler
app . post ( '/webhooks' , async ( req , res ) => {
// Verify signature first
if ( ! verifySignature ( req )) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
// Acknowledge receipt immediately
res . status ( 200 ). send ( 'OK' );
// Process asynchronously
processWebhook ( req . body ). catch ( err => {
console . error ( 'Webhook processing failed:' , err );
});
});
Best Practices
Verify signatures - Always validate HMAC signatures to prevent spoofing
Handle duplicates - Events may be delivered multiple times (use event IDs for deduplication)
Respond quickly - Acknowledge receipt within 30 seconds, process asynchronously
Monitor failures - Track failed deliveries and investigate issues
Use HTTPS - Webhook URLs must use HTTPS for security
Filter events - Only subscribe to events you need to reduce noise
Implement retries - Handle temporary failures gracefully on your end
Log events - Keep audit logs of received webhooks for debugging