Documentation Index
Fetch the complete documentation index at: https://mintlify.com/zitadel/zitadel/llms.txt
Use this file to discover all available pages before exploring further.
Webhooks in ZITADEL are implemented through Actions v2, allowing you to receive real-time notifications about events and customize ZITADEL’s behavior by calling external HTTP endpoints. This enables you to integrate ZITADEL with external systems, trigger custom workflows, and react to authentication and authorization events.
What are Webhooks?
Webhooks are HTTP callbacks that ZITADEL sends to your endpoints when specific events occur. Unlike polling, webhooks provide real-time notifications, enabling immediate reactions to changes in ZITADEL.
Use cases for webhooks:
- Sync user data to external systems when users are created or updated
- Trigger workflows when users authenticate or complete registration
- Send notifications to administrators when security events occur
- Validate data with external systems before allowing operations
- Enrich authentication flows with data from external sources
- Audit and log all authentication events to external systems
Actions v2 Architecture
Actions v2 consists of three components:
1. Endpoint
Your external HTTP service that receives webhook requests from ZITADEL. This can be any HTTP server capable of receiving POST requests.
2. Target
A ZITADEL resource containing information about how to call your endpoint:
- Endpoint URL
- Authentication method
- Timeout configuration
- Error handling behavior
3. Execution
A ZITADEL resource specifying when to call which targets:
- Condition type: Request, Response, Function, or Event
- Specific trigger: Which API call, event, or function triggers the webhook
- Target list: Which endpoints to call
Webhook Flow
- User performs an action (login, registration, API call, etc.)
- ZITADEL reaches a configured execution point
- ZITADEL calls all configured targets for that execution
- Your endpoint receives the webhook payload
- Your endpoint processes the request and returns a response
- ZITADEL continues or modifies its behavior based on the response
Execution Conditions
Actions v2 supports four types of execution conditions:
Request Executions
Trigger webhooks before ZITADEL processes an API request, allowing you to:
- Validate or reject requests
- Modify request data
- Enrich requests with external data
- Enforce custom authorization rules
Example use case: Validate that a user’s email domain is allowed before creating the user.
Response Executions
Trigger webhooks after ZITADEL processes an API request, allowing you to:
- React to successful operations
- Sync data to external systems
- Send notifications
- Trigger downstream workflows
Example use case: Sync newly created users to your CRM system.
Function Executions
Trigger webhooks during specific ZITADEL functions (replaces Actions v1), allowing you to:
- Customize authentication flows
- Modify token claims
- Enrich user data during login
- Implement custom authorization logic
Example use case: Add custom claims to access tokens based on external system data.
Event Executions
Trigger webhooks when ZITADEL events occur, allowing you to:
- Monitor system events in real-time
- Send notifications for security events
- Audit all authentication activity
- React to state changes
Example use case: Send alert when a user account is locked.
Creating a Target
Targets define how ZITADEL calls your webhook endpoint.
Target Configuration
| Property | Description |
|---|
name | Descriptive name for the target |
endpoint | URL of your webhook endpoint |
timeout | Maximum time to wait for response (e.g., ”10s”) |
restWebhook.interruptOnError | If true, ZITADEL flow stops on webhook failure |
restAsync | For asynchronous webhooks (fire-and-forget) |
Creating an Execution
Executions define when to trigger your webhooks.
Request Execution
Trigger webhook before creating a user:
curl -X PUT "https://${CUSTOM_DOMAIN}/v2/actions/executions" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
-d '{
"condition": {
"request": {
"method": "/zitadel.user.v2.UserService/AddHumanUser"
}
},
"targets": ["337246363446151234"]
}'
Response Execution
Trigger webhook after creating a user:
curl -X PUT "https://${CUSTOM_DOMAIN}/v2/actions/executions" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
-d '{
"condition": {
"response": {
"method": "/zitadel.user.v2.UserService/AddHumanUser"
}
},
"targets": ["337246363446151234"]
}'
Function Execution
Trigger webhook during token creation:
curl -X PUT "https://${CUSTOM_DOMAIN}/v2/actions/executions" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
-d '{
"condition": {
"function": {
"name": "complement_token"
}
},
"targets": ["337246363446151234"]
}'
Event Execution
Trigger webhook when user is locked:
curl -X PUT "https://${CUSTOM_DOMAIN}/v2/actions/executions" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
-d '{
"condition": {
"event": {
"event": "user.locked"
}
},
"targets": ["337246363446151234"]
}'
Webhook Payload
ZITADEL sends webhook payloads as JSON in the request body.
Request Execution Payload
{
"fullMethod": "/zitadel.user.v2.UserService/AddHumanUser",
"instanceID": "123456789012345678",
"orgID": "234567890123456789",
"projectID": "345678901234567890",
"userID": "456789012345678901",
"request": {
"user": {
"profile": {
"givenName": "John",
"familyName": "Doe"
},
"email": {
"email": "john.doe@example.com"
}
}
}
}
Response Execution Payload
{
"fullMethod": "/zitadel.user.v2.UserService/AddHumanUser",
"instanceID": "123456789012345678",
"orgID": "234567890123456789",
"projectID": "345678901234567890",
"userID": "567890123456789012",
"request": {
"user": {
"profile": {
"givenName": "John",
"familyName": "Doe"
},
"email": {
"email": "john.doe@example.com"
}
}
},
"response": {
"userId": "567890123456789012",
"details": {
"sequence": "1",
"changeDate": "2024-01-10T10:30:00Z"
}
}
}
Event Execution Payload
{
"event": "user.locked",
"instanceID": "123456789012345678",
"orgID": "234567890123456789",
"userID": "567890123456789012",
"eventData": {
"userId": "567890123456789012",
"userName": "john.doe@example.com",
"lockedAt": "2024-01-10T10:30:00Z",
"reason": "Too many failed login attempts"
}
}
Webhook Response
Your webhook endpoint should respond with appropriate HTTP status codes:
Successful Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success"
}
Error Response
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"status": "error",
"message": "Email domain not allowed"
}
If interruptOnError is true and your webhook returns an error, ZITADEL will abort the operation and return the error to the user.
Response with Modifications (Request Executions)
For request executions, you can modify the request data:
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"request": {
"user": {
"profile": {
"givenName": "John",
"familyName": "Doe",
"displayName": "John Doe (Modified)"
},
"email": {
"email": "john.doe@example.com"
},
"metadata": [
{
"key": "department",
"value": "Engineering"
}
]
}
}
}
Webhook Security
Signature Verification
ZITADEL signs all webhook requests with HMAC-SHA256. Verify signatures to ensure requests are from ZITADEL:
Signature is sent in the header:
X-Zitadel-Signature: sha256=SIGNATURE_VALUE
Verify signature in your webhook endpoint:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, signingKey) {
const hmac = crypto.createHmac('sha256', signingKey);
hmac.update(JSON.stringify(payload));
const expectedSignature = 'sha256=' + hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
// Express.js example
app.post('/webhooks/zitadel', (req, res) => {
const signature = req.headers['x-zitadel-signature'];
const signingKey = process.env.ZITADEL_SIGNING_KEY;
if (!verifyWebhookSignature(req.body, signature, signingKey)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
res.json({ status: 'success' });
});
IP Allowlisting
Restrict webhook endpoints to accept requests only from ZITADEL’s IP addresses.
HTTPS Required
Webhook endpoints must use HTTPS. ZITADEL will not send webhooks to HTTP endpoints.
You can add custom headers to webhook requests for additional authentication:
curl -X POST "https://${CUSTOM_DOMAIN}/v2/actions/targets" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
-d '{
"name": "Authenticated Webhook",
"restWebhook": {
"interruptOnError": true
},
"endpoint": "https://api.example.com/webhooks/zitadel",
"timeout": "10s"
}'
Implementing a Webhook Endpoint
Node.js/Express Example
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const SIGNING_KEY = process.env.ZITADEL_SIGNING_KEY;
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', SIGNING_KEY);
hmac.update(JSON.stringify(payload));
const expectedSignature = 'sha256=' + hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
app.post('/webhooks/zitadel/user-created', (req, res) => {
// Verify signature
const signature = req.headers['x-zitadel-signature'];
if (!verifySignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { fullMethod, userID, request, response } = req.body;
console.log('User created:', {
userId: userID,
email: request.user.email.email,
name: `${request.user.profile.givenName} ${request.user.profile.familyName}`
});
// Sync to external system
syncToExternalSystem({
zitadelUserId: userID,
email: request.user.email.email,
firstName: request.user.profile.givenName,
lastName: request.user.profile.familyName
});
res.json({ status: 'success' });
});
app.post('/webhooks/zitadel/validate-email', (req, res) => {
const { request } = req.body;
const email = request.user.email.email;
const allowedDomains = ['example.com', 'company.com'];
const domain = email.split('@')[1];
if (!allowedDomains.includes(domain)) {
return res.status(400).json({
status: 'error',
message: `Email domain ${domain} is not allowed`
});
}
res.json({ status: 'success' });
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
Python/Flask Example
import os
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
SIGNING_KEY = os.getenv('ZITADEL_SIGNING_KEY').encode()
def verify_signature(payload, signature):
expected = 'sha256=' + hmac.new(
SIGNING_KEY,
json.dumps(payload).encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/webhooks/zitadel/user-created', methods=['POST'])
def user_created():
signature = request.headers.get('X-Zitadel-Signature')
payload = request.json
if not verify_signature(payload, signature):
return jsonify({'error': 'Invalid signature'}), 401
user_id = payload['userID']
email = payload['request']['user']['email']['email']
print(f'User created: {user_id} ({email})')
# Sync to external system
sync_to_external_system(payload)
return jsonify({'status': 'success'})
if __name__ == '__main__':
app.run(port=3000)
Testing Webhooks
Local Testing with Webhook.site
Use Webhook.site with XHR redirect to test webhooks locally:
- Open webhook.site and get your unique URL
- Configure XHR redirect to your local endpoint (e.g.,
http://localhost:3000/webhook)
- Create a target in ZITADEL pointing to your webhook.site URL
- Add CORS headers to your local endpoint
- Keep webhook.site tab open while testing
Use Webhook.site with XHR redirect to test webhooks locally during development.
Using ngrok
Expose your local server to the internet:
# Install ngrok
npm install -g ngrok
# Expose local port 3000
ngrok http 3000
# Use the generated HTTPS URL as your webhook endpoint
# Example: https://abc123.ngrok.io/webhooks/zitadel
Common Use Cases
User Provisioning to External System
app.post('/webhooks/zitadel/user-created', async (req, res) => {
const { userID, request } = req.body;
await fetch('https://crm.example.com/api/contacts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CRM_API_KEY}`
},
body: JSON.stringify({
externalId: userID,
email: request.user.email.email,
firstName: request.user.profile.givenName,
lastName: request.user.profile.familyName
})
});
res.json({ status: 'success' });
});
Email Domain Validation
app.post('/webhooks/zitadel/validate-user', (req, res) => {
const email = req.body.request.user.email.email;
const allowedDomains = process.env.ALLOWED_DOMAINS.split(',');
const domain = email.split('@')[1];
if (!allowedDomains.includes(domain)) {
return res.status(400).json({
status: 'error',
message: `Registration restricted to ${allowedDomains.join(', ')}`
});
}
res.json({ status: 'success' });
});
Enrich Token Claims
app.post('/webhooks/zitadel/enrich-token', async (req, res) => {
const { userID } = req.body;
// Fetch user data from external system
const response = await fetch(
`https://api.example.com/users/${userID}`,
{
headers: { 'Authorization': `Bearer ${API_KEY}` }
}
);
const userData = await response.json();
// Return modified response with additional claims
res.json({
status: 'success',
claims: {
department: userData.department,
employee_id: userData.employeeId,
manager_email: userData.managerEmail
}
});
});
Security Event Notification
app.post('/webhooks/zitadel/user-locked', async (req, res) => {
const { userID, eventData } = req.body;
// Send notification to security team
await fetch('https://api.slack.com/webhooks/WEBHOOK_ID', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `User account locked: ${eventData.userName}`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*User Account Locked*\n*User:* ${eventData.userName}\n*Reason:* ${eventData.reason}\n*Time:* ${eventData.lockedAt}`
}
}
]
})
});
res.json({ status: 'success' });
});
Best Practices
- Verify signatures: Always validate webhook signatures
- Use HTTPS: Never expose webhook endpoints over HTTP
- Implement idempotency: Handle duplicate webhooks gracefully
- Respond quickly: Process webhooks asynchronously if needed
- Return proper status codes: Use appropriate HTTP codes
- Log all webhooks: Keep audit trail of webhook calls
- Monitor failures: Track and alert on webhook errors
- Set appropriate timeouts: Balance between reliability and performance
- Handle errors gracefully: Decide when to interrupt vs. allow to fail
- Test thoroughly: Validate all webhook scenarios before production
Troubleshooting
Webhook not received:
- Check endpoint URL is correct and accessible
- Verify HTTPS certificate is valid
- Check firewall rules allow ZITADEL IP addresses
- Ensure execution is properly configured
Signature verification fails:
- Verify signing key matches the one from target creation
- Ensure payload is not modified before verification
- Check charset encoding (use UTF-8)
Webhook times out:
- Reduce processing time in webhook handler
- Move heavy processing to background jobs
- Increase timeout in target configuration
Flow interrupted unexpectedly:
- Check webhook response status codes
- Review
interruptOnError setting
- Check webhook endpoint logs for errors
Resources