SaaS User Onboarding Pipeline
This use case demonstrates how to build a complete user onboarding pipeline by routing Clerk authentication webhooks to multiple destinations—welcome email via Resend, Slack team notification, HubSpot contact creation, and an internal onboarding service—each with custom transforms that enrich and reshape user data.
Architecture
flowchart LR
Clerk[Clerk Auth Webhooks] --> Source[Hookbase Source]
Source --> R1[Route: Welcome Email]
Source --> R2[Route: Team Notification]
Source --> R3[Route: CRM Sync]
Source --> R4[Route: Internal Onboarding]
R1 --> |Transform| D1[Resend Email API]
R2 --> |Transform| D2[Slack Channel]
R3 --> |Transform| D3[HubSpot Contacts API]
R4 --> |Transform| D4[Internal Webhook]Flow:
- Clerk sends
user.createdwebhook to Hookbase source endpoint - Source verifies Svix signature (Clerk uses Svix for webhook delivery)
- Four routes evaluate the event in parallel
- Each route applies filters and transforms to shape user data for the destination
- Transformed payloads delivered to respective services simultaneously
Step 1: Create the Source
Create a Clerk source with Svix signature verification:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/sources \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk Production",
"slug": "clerk-prod",
"description": "Production Clerk authentication webhooks",
"verificationConfig": {
"type": "svix",
"secret": "whsec_your_clerk_webhook_signing_secret"
}
}'Response:
{
"data": {
"id": "src_clerk01",
"name": "Clerk Production",
"slug": "clerk-prod",
"url": "https://api.hookbase.app/ingest/your-org/clerk-prod",
"verificationConfig": {
"type": "svix"
},
"createdAt": "2026-03-07T10:00:00Z"
}
}TIP
Configure this URL in your Clerk Dashboard under Webhooks → Add Endpoint. Select the user.created event type, and copy the Signing Secret into the verificationConfig.secret field above.
Step 2: Create Destinations
Create four destinations for the onboarding pipeline:
Resend Email API Destination
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Resend Welcome Email",
"type": "http",
"config": {
"url": "https://api.resend.com/emails",
"method": "POST",
"headers": {
"Authorization": "Bearer re_YOUR_RESEND_API_KEY",
"Content-Type": "application/json"
}
},
"retryConfig": {
"maxAttempts": 5,
"backoffMultiplier": 2,
"initialInterval": 2000
}
}'Slack Team Notification Destination
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Slack #new-signups",
"type": "slack",
"config": {
"url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
},
"retryConfig": {
"maxAttempts": 2,
"backoffMultiplier": 1.5,
"initialInterval": 500
}
}'HubSpot Contacts API Destination
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "HubSpot Contact Creation",
"type": "http",
"config": {
"url": "https://api.hubapi.com/crm/v3/objects/contacts",
"method": "POST",
"headers": {
"Authorization": "Bearer YOUR_HUBSPOT_ACCESS_TOKEN",
"Content-Type": "application/json"
}
},
"retryConfig": {
"maxAttempts": 3,
"backoffMultiplier": 2,
"initialInterval": 1000
}
}'Internal Onboarding Webhook Destination
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Internal Onboarding Service",
"type": "http",
"config": {
"url": "https://api.yourcompany.com/internal/onboarding/webhook",
"method": "POST",
"headers": {
"Authorization": "Bearer YOUR_INTERNAL_API_KEY",
"Content-Type": "application/json",
"X-Source": "hookbase"
}
},
"retryConfig": {
"maxAttempts": 5,
"backoffMultiplier": 2,
"initialInterval": 1000
}
}'Step 3: Create Routes with Filters
All four routes share a common filter on the user.created event type. Some routes add additional conditions.
Shared Filter: User Created Events
Create a base filter that matches only user.created events:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "User Created Events",
"conditions": [
{
"field": "type",
"operator": "eq",
"value": "user.created"
}
],
"logic": "AND"
}'Welcome Email Route
Route new user events to Resend for welcome emails:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk → Welcome Email",
"sourceId": "src_clerk01",
"destinationId": "dst_resend01",
"filterId": "flt_usercreated01",
"transformId": "tfm_resend01",
"enabled": true
}'Slack Notification Route
Notify the team about every new signup:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk → Slack New Signup",
"sourceId": "src_clerk01",
"destinationId": "dst_slack01",
"filterId": "flt_usercreated01",
"transformId": "tfm_slack01",
"enabled": true
}'HubSpot Contact Route
Create a CRM contact for users who signed up with a business email domain:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "User Created - Business Email",
"conditions": [
{
"field": "type",
"operator": "eq",
"value": "user.created"
},
{
"field": "data.email_addresses[0].email_address",
"operator": "not_matches",
"value": "@(gmail|yahoo|hotmail|outlook|icloud)\\."
}
],
"logic": "AND"
}'TIP
This filter excludes personal email domains from CRM sync, reducing noise in your HubSpot pipeline. Adjust the regex pattern to match your target audience.
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk → HubSpot Contact",
"sourceId": "src_clerk01",
"destinationId": "dst_hubspot01",
"filterId": "flt_bizuser01",
"transformId": "tfm_hubspot01",
"enabled": true
}'Internal Onboarding Route
Send enriched user data to your internal onboarding service for provisioning:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk → Internal Onboarding",
"sourceId": "src_clerk01",
"destinationId": "dst_internal01",
"filterId": "flt_usercreated01",
"transformId": "tfm_internal01",
"enabled": true
}'Step 4: Add Transforms
Each destination needs user data shaped into its specific format. The transforms extract and enrich data from the Clerk user.created event payload.
Resend Email Transform
Format user data into the Resend email API payload:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk → Resend Welcome Email",
"type": "jsonata",
"config": {
"expression": "(\n $email := data.email_addresses[0].email_address;\n $firstName := data.first_name ? data.first_name : \"there\";\n $fullName := data.first_name & (data.last_name ? \" \" & data.last_name : \"\");\n {\n \"from\": \"[email protected]\",\n \"to\": [$email],\n \"subject\": \"Welcome to YourApp, \" & $firstName & \"!\",\n \"html\": \"<h1>Welcome aboard, \" & $firstName & \"!</h1><p>We're thrilled to have you join us. Here are a few things to get you started:</p><ul><li><a href=\\\"https://app.yourcompany.com/getting-started\\\">Quick Start Guide</a></li><li><a href=\\\"https://docs.yourcompany.com\\\">Documentation</a></li><li><a href=\\\"https://app.yourcompany.com/settings/profile\\\">Complete Your Profile</a></li></ul><p>If you have any questions, reply to this email — we read every message.</p><p>— The YourApp Team</p>\",\n \"tags\": [\n {\n \"name\": \"category\",\n \"value\": \"onboarding\"\n },\n {\n \"name\": \"user_id\",\n \"value\": data.id\n }\n ]\n }\n)"
}
}'Example Output:
{
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "Welcome to YourApp, Jane!",
"html": "<h1>Welcome aboard, Jane!</h1><p>We're thrilled to have you join us. Here are a few things to get you started:</p><ul><li><a href=\"https://app.yourcompany.com/getting-started\">Quick Start Guide</a></li><li><a href=\"https://docs.yourcompany.com\">Documentation</a></li><li><a href=\"https://app.yourcompany.com/settings/profile\">Complete Your Profile</a></li></ul><p>If you have any questions, reply to this email — we read every message.</p><p>— The YourApp Team</p>",
"tags": [
{
"name": "category",
"value": "onboarding"
},
{
"name": "user_id",
"value": "user_2abc123def456"
}
]
}Slack Block Kit Transform
Format as a rich Slack Block Kit message with user details:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk → Slack Welcome Block",
"type": "jsonata",
"config": {
"expression": "(\n $email := data.email_addresses[0].email_address;\n $firstName := data.first_name ? data.first_name : \"Unknown\";\n $lastName := data.last_name ? data.last_name : \"\";\n $fullName := $firstName & ($lastName ? \" \" & $lastName : \"\");\n $domain := $substringAfter($email, \"@\");\n $provider := data.external_accounts ? data.external_accounts[0].provider : \"email\";\n $avatar := data.image_url ? data.image_url : \"\";\n $timestamp := $floor(data.created_at / 1000);\n {\n \"attachments\": [\n {\n \"color\": \"#36a64f\",\n \"blocks\": [\n {\n \"type\": \"header\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \":tada: New User Signup\"\n }\n },\n {\n \"type\": \"section\",\n \"fields\": [\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Name:*\\n\" & $fullName\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Email:*\\n\" & $email\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Domain:*\\n\" & $domain\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Auth Provider:*\\n\" & $provider\n }\n ]\n },\n {\n \"type\": \"context\",\n \"elements\": [\n {\n \"type\": \"mrkdwn\",\n \"text\": \"User ID: \" & data.id & \" | Signed up <!date^\" & $string($timestamp) & \"^{date_short_pretty} at {time}|recently>\"\n }\n ]\n }\n ]\n }\n ]\n }\n)"
}
}'Example Output (Slack Message):
{
"attachments": [
{
"color": "#36a64f",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":tada: New User Signup"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Name:*\nJane Doe"
},
{
"type": "mrkdwn",
"text": "*Email:*\n[email protected]"
},
{
"type": "mrkdwn",
"text": "*Domain:*\nacme.com"
},
{
"type": "mrkdwn",
"text": "*Auth Provider:*\ngoogle"
}
]
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "User ID: user_2abc123def456 | Signed up <!date^1709816400^{date_short_pretty} at {time}|recently>"
}
]
}
]
}
]
}HubSpot Contact Transform
Reshape into HubSpot CRM contact creation payload:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk → HubSpot Contact",
"type": "jsonata",
"config": {
"expression": "(\n $email := data.email_addresses[0].email_address;\n $domain := $substringAfter($email, \"@\");\n $provider := data.external_accounts ? data.external_accounts[0].provider : \"email\";\n {\n \"properties\": {\n \"email\": $email,\n \"firstname\": data.first_name ? data.first_name : \"\",\n \"lastname\": data.last_name ? data.last_name : \"\",\n \"company\": $domain,\n \"website\": \"https://\" & $domain,\n \"lifecyclestage\": \"subscriber\",\n \"hs_lead_status\": \"NEW\",\n \"signup_source\": \"web_app\",\n \"auth_provider\": $provider,\n \"clerk_user_id\": data.id,\n \"signup_date\": $fromMillis(data.created_at, \"[Y0001]-[M01]-[D01]\")\n }\n }\n)"
}
}'Example Output:
{
"properties": {
"email": "[email protected]",
"firstname": "Jane",
"lastname": "Doe",
"company": "acme.com",
"website": "https://acme.com",
"lifecyclestage": "subscriber",
"hs_lead_status": "NEW",
"signup_source": "web_app",
"auth_provider": "google",
"clerk_user_id": "user_2abc123def456",
"signup_date": "2026-03-07"
}
}TIP
Create the custom properties signup_source, auth_provider, clerk_user_id, and signup_date in HubSpot under Settings → Properties → Contact Properties before enabling this route.
Internal Onboarding Transform
Build an enriched user object for your internal service with all available data:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk → Internal Enriched User",
"type": "jsonata",
"config": {
"expression": "(\n $email := data.email_addresses[0].email_address;\n $domain := $substringAfter($email, \"@\");\n $isBusinessEmail := $not($contains($email, \"@gmail.\") or $contains($email, \"@yahoo.\") or $contains($email, \"@hotmail.\") or $contains($email, \"@outlook.\") or $contains($email, \"@icloud.\"));\n $provider := data.external_accounts ? data.external_accounts[0].provider : \"email\";\n {\n \"action\": \"provision_user\",\n \"user\": {\n \"externalId\": data.id,\n \"email\": $email,\n \"emailVerified\": data.email_addresses[0].verification.status = \"verified\",\n \"firstName\": data.first_name,\n \"lastName\": data.last_name,\n \"fullName\": data.first_name & (data.last_name ? \" \" & data.last_name : \"\"),\n \"avatarUrl\": data.image_url,\n \"authProvider\": $provider\n },\n \"organization\": {\n \"domain\": $domain,\n \"isBusinessDomain\": $isBusinessEmail\n },\n \"onboarding\": {\n \"plan\": \"free\",\n \"trialEndsAt\": $fromMillis(data.created_at + 1209600000, \"[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]Z\"),\n \"features\": [\"dashboard\", \"api_access\", \"single_source\"],\n \"steps\": [\n {\"key\": \"verify_email\", \"completed\": data.email_addresses[0].verification.status = \"verified\"},\n {\"key\": \"complete_profile\", \"completed\": false},\n {\"key\": \"create_first_source\", \"completed\": false},\n {\"key\": \"send_test_event\", \"completed\": false}\n ]\n },\n \"metadata\": {\n \"source\": \"clerk\",\n \"webhookEventId\": $$.id,\n \"timestamp\": $fromMillis(data.created_at, \"[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]Z\")\n }\n }\n)"
}
}'Example Output:
{
"action": "provision_user",
"user": {
"externalId": "user_2abc123def456",
"email": "[email protected]",
"emailVerified": true,
"firstName": "Jane",
"lastName": "Doe",
"fullName": "Jane Doe",
"avatarUrl": "https://img.clerk.com/abc123",
"authProvider": "google"
},
"organization": {
"domain": "acme.com",
"isBusinessDomain": true
},
"onboarding": {
"plan": "free",
"trialEndsAt": "2026-03-21T10:00:00Z",
"features": ["dashboard", "api_access", "single_source"],
"steps": [
{"key": "verify_email", "completed": true},
{"key": "complete_profile", "completed": false},
{"key": "create_first_source", "completed": false},
{"key": "send_test_event", "completed": false}
]
},
"metadata": {
"source": "clerk",
"webhookEventId": "evt_2xyz789ghi012",
"timestamp": "2026-03-07T10:00:00Z"
}
}Step 5: Test the Pipeline
Send a test Clerk-like user.created payload to verify all four routes:
curl -X POST https://api.hookbase.app/ingest/your-org/clerk-prod \
-H "Content-Type: application/json" \
-H "svix-id: msg_test_123" \
-H "svix-timestamp: 1709816400" \
-H "svix-signature: v1=test_signature" \
-d '{
"id": "evt_2xyz789ghi012",
"type": "user.created",
"data": {
"id": "user_2abc123def456",
"first_name": "Jane",
"last_name": "Doe",
"email_addresses": [
{
"id": "idn_abc123",
"email_address": "[email protected]",
"verification": {
"status": "verified",
"strategy": "from_oauth_google"
}
}
],
"primary_email_address_id": "idn_abc123",
"image_url": "https://img.clerk.com/abc123",
"external_accounts": [
{
"provider": "google",
"email_address": "[email protected]",
"first_name": "Jane",
"last_name": "Doe"
}
],
"created_at": 1709816400000,
"updated_at": 1709816400000
}
}'TIP
For local development testing, disable signature verification temporarily or use the Clerk CLI to send real test events: clerk webhooks trigger user.created
Verify Deliveries:
curl https://api.hookbase.app/api/organizations/{orgId}/deliveries?limit=10 \
-H "Authorization: Bearer YOUR_TOKEN"You should see 4 deliveries (or 3 if the HubSpot filter excluded a personal email):
- Delivery to Resend with welcome email payload
- Delivery to Slack with Block Kit signup notification
- Delivery to HubSpot with contact creation payload (business emails only)
- Delivery to Internal Onboarding Service with enriched user object
Check individual delivery details:
curl https://api.hookbase.app/api/organizations/{orgId}/deliveries/dlv_abc123 \
-H "Authorization: Bearer YOUR_TOKEN"Production Considerations
1. Enable Deduplication
Prevent duplicate onboarding actions if Clerk retries the webhook:
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/sources/src_clerk01 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"deduplicationConfig": {
"enabled": true,
"keyPath": "data.id",
"windowSeconds": 86400
}
}'WARNING
Deduplicating on data.id (the Clerk user ID) ensures that even if Clerk retries the user.created event multiple times, your user only receives one welcome email and one onboarding provisioning call.
2. Set Up Failover for Welcome Email
Ensure new users always receive their welcome email by adding a backup email provider:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Resend Backup (SendGrid)",
"type": "http",
"config": {
"url": "https://api.sendgrid.com/v3/mail/send",
"method": "POST",
"headers": {
"Authorization": "Bearer YOUR_SENDGRID_API_KEY",
"Content-Type": "application/json"
}
}
}'curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/routes/rte_resend01 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"failoverConfig": {
"enabled": true,
"destinationId": "dst_sendgrid01",
"triggerAfterAttempts": 3
}
}'3. Configure Circuit Breaker
Protect HubSpot and internal services from overload during signup spikes (e.g., Product Hunt launch day):
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/destinations/dst_hubspot01 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"circuitBreakerConfig": {
"enabled": true,
"failureThreshold": 10,
"windowSeconds": 60,
"resetTimeoutSeconds": 300
}
}'4. Set Up Notification Channels
Get alerted when critical onboarding deliveries fail:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/notification-channels \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Onboarding Pipeline Alerts",
"type": "slack",
"config": {
"url": "https://hooks.slack.com/services/YOUR/ALERT/URL"
},
"events": ["delivery.failed", "destination.circuit_breaker.opened"],
"filters": {
"destinationIds": ["dst_resend01", "dst_internal01"]
}
}'5. Use Scoped API Keys
Create a dedicated API key for the Clerk webhook ingestion endpoint:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/api-keys \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Clerk Webhook Ingestion",
"scopes": ["events:create"],
"sourceIds": ["src_clerk01"],
"expiresAt": "2027-03-07T00:00:00Z"
}'Use the returned API key in your Clerk webhook configuration instead of your user token.
6. Monitor Pipeline Health
Track the overall health of your onboarding pipeline with periodic checks:
curl https://api.hookbase.app/api/organizations/{orgId}/analytics/routes?period=24h \
-H "Authorization: Bearer YOUR_TOKEN"Key metrics to watch:
- Delivery success rate — Should stay above 99% for email and internal routes
- Average latency — Welcome emails should arrive within seconds of signup
- HubSpot contact creation rate — Compare against total signups to verify filter accuracy
- Queue depth — Spikes may indicate downstream service issues
You can also set up a cron-based health check to periodically verify that all four destinations are reachable:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/cron-jobs \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Onboarding Pipeline Health Check",
"schedule": "*/15 * * * *",
"destinationIds": ["dst_resend01", "dst_slack01", "dst_hubspot01", "dst_internal01"],
"notificationChannelId": "nch_alerts01"
}'This runs every 15 minutes and alerts your team if any onboarding destination becomes unreachable.
Related Guides
- Transforms - JSONata syntax and advanced transform examples
- Filters - Complex routing logic and filter conditions
- Deduplication - Prevent duplicate event processing
- Failover - Ensure critical events are always delivered
- Circuit Breaker - Protect downstream services from overload
- Notification Channels - Configure delivery failure alerts
- Pipeline Architecture - Design multi-destination webhook pipelines
See Also
- Clerk Integration — Clerk webhook setup
- Transforms — Reshape user data
- Routes — Multi-destination routing
- Customer Data Sync — Related use case