Webhooks
Configure secure HTTP endpoints that trigger workflows in response to external events.
What are Webhooks?
Webhooks enable external systems to trigger M3 Forge workflows by sending HTTP POST requests to unique URLs. Each webhook trigger generates a dedicated endpoint that:
- Accepts JSON payloads
- Validates authentication credentials
- Triggers the configured workflow
- Passes the payload as workflow input
This pattern enables integrations with:
- CI/CD systems (GitHub Actions, GitLab CI, Jenkins)
- Payment processors (Stripe, PayPal)
- Communication platforms (Slack, Discord, email services)
- Monitoring tools (Datadog, PagerDuty)
- Custom applications (your own services)

Creating a Webhook
Create Webhook Trigger
In the Automation page or workflow START node, select “Webhook” as trigger type.
Configure Authentication
Choose authentication method:
- None - Public endpoint (not recommended for production)
- Bearer Token - Shared secret in Authorization header
- HMAC SHA-256 - Signature-based verification (most secure)
Get Webhook URL
M3 Forge generates a unique URL:
https://your-instance/api/webhooks/wh_abc123xyzCopy this URL to configure in the external system.
Test the Webhook
Send a test request to verify configuration:
curl -X POST https://your-instance/api/webhooks/wh_abc123xyz \
-H "Authorization: Bearer YOUR_SECRET" \
-H "Content-Type: application/json" \
-d '{"test": true}'Check the Automation page for a successful tick and triggered run.
Authentication Methods
None (Public Webhook)
No authentication required. Anyone with the URL can trigger the workflow.
{
"auth_type": "none"
}Request:
curl -X POST https://your-instance/api/webhooks/wh_abc123 \
-H "Content-Type: application/json" \
-d '{"key": "value"}'Public webhooks are vulnerable to abuse. Only use for non-sensitive testing or when webhook URL is kept secret. For production, always use bearer token or HMAC authentication.
Bearer Token
Shared secret in the Authorization header.
{
"auth_type": "bearer_token",
"secret": "your-secret-token"
}Request:
curl -X POST https://your-instance/api/webhooks/wh_abc123 \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{"key": "value"}'Secret generation:
- Auto-generate via UI (32-character random string)
- Provide custom secret (minimum 16 characters)
- Store in password manager or secrets vault
Best practices:
- Use cryptographically random secrets (not dictionary words)
- Rotate secrets periodically (every 90 days)
- Never commit secrets to version control
- Use different secrets for dev/staging/production
HMAC SHA-256
Cryptographic signature computed from payload and secret.
{
"auth_type": "hmac_sha256",
"secret": "your-hmac-secret"
}Request:
cURL
PAYLOAD='{"key":"value"}'
SECRET="your-hmac-secret"
# Compute signature
SIGNATURE=$(echo -n "$PAYLOAD" | \
openssl dgst -sha256 -hmac "$SECRET" | \
awk '{print $2}')
# Send request
curl -X POST https://your-instance/api/webhooks/wh_abc123 \
-H "X-Webhook-Signature: sha256=$SIGNATURE" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"Advantages over bearer token:
- Payload integrity - Detects tampering (modified payload invalidates signature)
- Replay protection - Include timestamp in payload, reject old requests
- Secret rotation - Can verify with old and new secrets during rotation
Security properties:
- Signature computed from exact payload bytes (order matters)
- Any modification to payload invalidates signature
- Secret never transmitted (only signature sent)
- Signature cannot be forged without secret
IP Allowlisting
Restrict webhook sources to known IP addresses.
{
"auth_type": "bearer_token",
"secret": "your-secret",
"allowed_ips": [
"203.0.113.0/24",
"198.51.100.42"
]
}CIDR notation:
203.0.113.0/24- Range from 203.0.113.0 to 203.0.113.255198.51.100.42/32- Single IP (same as198.51.100.42)0.0.0.0/0- Allow all IPs (defeats the purpose)
Finding source IPs:
- GitHub: GitHub webhook IPs (check
hooksfield) - Stripe: Stripe webhook IPs
- Custom services: Check server logs for request source IP
IP allowlisting provides defense in depth but is not a replacement for authentication. Use both for maximum security.
Payload Format
Webhook body becomes workflow input data.
Request:
{
"event": "user.signup",
"user": {
"id": 12345,
"email": "user@example.com"
},
"timestamp": "2024-03-19T10:30:00Z"
}Workflow receives:
{
"data": {
"event": "user.signup",
"user": {
"id": 12345,
"email": "user@example.com"
},
"timestamp": "2024-03-19T10:30:00Z"
},
"metadata": {
"trigger_type": "webhook",
"webhook_id": "wh_abc123xyz",
"source_ip": "203.0.113.42",
"received_at": "2024-03-19T10:30:01Z",
"headers": {
"user-agent": "GitHub-Hookshot/abc123",
"x-github-event": "push"
}
}
}Access payload fields in nodes via JSONPath:
$.data.event→"user.signup"$.data.user.email→"user@example.com"$.metadata.source_ip→"203.0.113.42"
Content Types
Supported Content-Type headers:
| Type | Processing |
|---|---|
application/json | Parse as JSON object |
application/x-www-form-urlencoded | Parse form data to JSON |
text/plain | Store raw text in $.data.body |
multipart/form-data | Not supported (use file upload endpoints) |
Most webhook sources send application/json.
Size Limits
- Maximum payload size: 10 MB
- Request timeout: 30 seconds
- Rate limit: 100 requests per minute per webhook ID
Exceeding limits returns HTTP 413 (Payload Too Large) or 429 (Too Many Requests).
Response Codes
Webhook requests receive immediate HTTP responses:
| Code | Meaning | Description |
|---|---|---|
| 200 OK | Success | Workflow triggered, run ID returned |
| 400 Bad Request | Invalid payload | Malformed JSON or missing required fields |
| 401 Unauthorized | Auth failed | Invalid or missing credentials |
| 403 Forbidden | IP blocked | Source IP not in allowlist |
| 404 Not Found | Webhook not found | Invalid webhook ID |
| 413 Payload Too Large | Size exceeded | Payload > 10 MB |
| 429 Too Many Requests | Rate limited | Exceeded 100 req/min |
| 500 Internal Server Error | Server error | M3 Forge backend issue |
Success response:
{
"status": "success",
"run_id": "run_abc123xyz",
"workflow_id": "extract-pipeline",
"message": "Workflow triggered successfully"
}Error response:
{
"status": "error",
"error": "Invalid authentication credentials",
"code": "WEBHOOK_AUTH_FAILED"
}Retry Behavior
Webhook sources typically retry failed requests:
| Service | Retry Logic | Timeout |
|---|---|---|
| GitHub | 3 retries with exponential backoff | 10s, 30s, 60s |
| Stripe | 3 retries over 3 days | Exponential backoff |
| Custom | Varies | Configure in sender |
Best practices:
- Return 200 OK as quickly as possible (within 5 seconds)
- Workflow execution is async (don’t wait for completion)
- Log all webhook requests for debugging
- Monitor retry counts to detect issues
Common Integrations
GitHub Push Events
Trigger workflow on code commits.
GitHub webhook configuration:
- Go to repository Settings → Webhooks → Add webhook
- Payload URL:
https://your-instance/api/webhooks/wh_abc123 - Content type:
application/json - Secret: Your bearer token or HMAC secret
- Events: Select “Just the push event”
Payload example:
{
"ref": "refs/heads/main",
"repository": {
"name": "my-repo",
"full_name": "user/my-repo"
},
"commits": [
{
"id": "abc123",
"message": "Update README",
"author": {
"name": "Jane Doe",
"email": "jane@example.com"
}
}
]
}Workflow access:
- Branch:
$.data.ref - Repo:
$.data.repository.name - Commit message:
$.data.commits[0].message
Stripe Payment Events
Trigger workflow on successful payments.
Stripe webhook configuration:
- Dashboard → Developers → Webhooks → Add endpoint
- Endpoint URL:
https://your-instance/api/webhooks/wh_abc123 - Events to send:
payment_intent.succeeded - Webhook signing secret: Use for HMAC verification
Payload example:
{
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_abc123",
"amount": 5000,
"currency": "usd",
"customer": "cus_xyz789"
}
}
}Workflow access:
- Amount:
$.data.data.object.amount - Customer:
$.data.data.object.customer
Slack Slash Commands
Trigger workflow from Slack messages.
Slack app configuration:
- Create Slack app at api.slack.com/apps
- Features → Slash Commands → Create New Command
- Request URL:
https://your-instance/api/webhooks/wh_abc123 - Command:
/process-document
Payload example:
{
"token": "verification-token",
"command": "/process-document",
"text": "https://example.com/doc.pdf",
"user_id": "U12345678",
"channel_id": "C87654321"
}Workflow access:
- Document URL:
$.data.text - User:
$.data.user_id
Response:
Slack expects a response within 3 seconds. Return 200 OK immediately, then send results via Slack API later.
Debugging Failed Webhooks
View Webhook Logs
Webhook requests are logged with:
- Request timestamp
- Source IP
- Payload (truncated if > 1KB)
- Response code
- Error message (if failed)
Access logs in:
- Automation page → Select webhook sensor
- View tick timeline
- Click failed tick for details
Common Issues
401 Unauthorized:
- Verify
Authorizationheader is included - Check bearer token matches configured secret
- For HMAC, ensure signature computed correctly
403 Forbidden:
- Source IP not in allowlist
- Check sender IP:
curl https://api.ipify.org - Add IP to
allowed_ipsarray
400 Bad Request:
- Payload is not valid JSON
- Test with:
echo $PAYLOAD | jq .to validate JSON - Check
Content-Typeheader isapplication/json
Workflow not triggering:
- Webhook returns 200 OK but no run created
- Check sensor is active (not paused/disabled)
- Verify
target_workflow_idexists - Review sensor tick logs for errors
Testing Tools
Manual test:
# Simple test
curl -X POST https://your-instance/api/webhooks/wh_abc123 \
-H "Authorization: Bearer SECRET" \
-H "Content-Type: application/json" \
-d '{"test": true}'
# With verbose output
curl -v -X POST https://your-instance/api/webhooks/wh_abc123 \
-H "Authorization: Bearer SECRET" \
-H "Content-Type: application/json" \
-d @payload.jsonWebhook testing services:
- webhook.site - Inspect requests
- ngrok - Local development tunneling
- Postman - API testing
M3 Forge test mode:
Click “Test Webhook” in sensor detail view to trigger manually without external request.
Security Best Practices
Authentication
- Always use authentication in production (bearer token or HMAC)
- Rotate secrets every 90 days
- Use HTTPS only (reject HTTP requests)
- Validate payload schema in workflow
Network Security
- IP allowlist when source IPs are known and stable
- Rate limiting to prevent abuse (built-in: 100 req/min)
- Firewall rules to restrict access to webhook endpoints
Payload Validation
Add validation in workflow START node:
{
"type": "code",
"config": {
"language": "python",
"code": "assert 'user' in data, 'Missing user field'\nassert data['user']['id'] > 0, 'Invalid user ID'"
}
}Fail fast on malformed payloads.
Monitoring
- Alert on failures (repeated 401/403 errors)
- Track unusual patterns (sudden spike in requests)
- Log all requests for audit trail
- Monitor latency (slow responses may indicate attack)
Performance Optimization
Async Processing
Workflows execute asynchronously. Webhook returns 200 OK immediately, before workflow completes.
Timeline:
- Webhook request arrives (0ms)
- Authentication validated (5ms)
- Workflow queued (10ms)
- 200 OK returned to sender (10ms)
- Workflow executes (seconds to minutes later)
This ensures webhook sources don’t timeout waiting for workflow completion.
Payload Size
Large payloads (> 1 MB) increase processing time:
- Prefer references - Send URLs, not full content
- Compress data - Use gzip encoding
- Paginate - Send multiple small webhooks instead of one large
Idempotency
Webhook sources may retry failed requests, causing duplicate processing.
Make workflows idempotent:
# Check if already processed
existing = db.query(f"SELECT * FROM jobs WHERE webhook_id = '{webhook_id}'")
if existing:
return {"status": "already_processed"}
# Process and record
process_data(data)
db.insert(f"INSERT INTO jobs (webhook_id, ...) VALUES ('{webhook_id}', ...)")Use webhook ID or request ID as deduplication key.
Next Steps
- Configure trigger types for other automation patterns
- Set up scheduling for time-based execution
- Monitor webhook health in the Automation dashboard
- Integrate webhooks into workflows for real-time processing