Syncing Product Orgs from a CDP
Overview
Keep product org data current in TrailSpark by sending webhook calls from your CDP whenever org-level attributes change — plan tier, user count, MRR, trial status, or feature adoption. This is a lightweight alternative to building a dedicated ETL integration and works with any platform that can fire HTTP webhooks.
This guide covers setup for Segment, Rudderstack, and Hightouch. For the full endpoint reference and payload template configuration, see Product Org Updates.
Prerequisites
- Create an API key at Settings > API Keys with endpoint type Product Org Updates
- Configure the Payload Template to map fields from your CDP's payload format to TrailSpark fields
- Copy the API key — it is only displayed once
Your webhook endpoint:
POST https://{subdomain}.trailspark.ai/api/product-orgs/webhook/{apiKey}
Replace {subdomain} with your organization's subdomain and {apiKey} with the key you created.
Segment
Segment's group call is designed for org-level data. Use the Webhooks (Actions) destination to forward group calls to TrailSpark's product org endpoint.
The classic Webhooks destination is in maintenance mode. Use Webhooks (Actions) for new setups.
Adding the Destination
- In Segment, go to Connections > Catalog > Destinations
- Search for Webhooks (Actions) and select it
- Choose the source that sends your group calls
- Name the destination (e.g., "TrailSpark Product Orgs")
- Enable the destination
Creating the Mapping
- Go to the Mappings tab and click + New Mapping
- Select the Send action
- Set the trigger condition: Event type is group — this filters out track, identify, and page calls so only org-level data reaches this endpoint
- Configure the request:
| Setting | Value |
|---|---|
| URL | https://{subdomain}.trailspark.ai/api/product-orgs/webhook/{apiKey} |
| Method | POST |
| Content-Type | application/json |
| Headers | Add X-Api-Secret: {secret} if your API key has a secret |
- Select Send the full event as the body (TrailSpark's payload template handles field extraction)
- Save and enable the mapping
Instrumenting Group Calls
Fire a group call in your product when scoring-relevant attributes change. Do not call group on every request or page load — only when a value TrailSpark cares about actually changes.
Good trigger points:
- Subscription webhook handler (plan change, trial start/end)
- User invite or removal endpoint (user count change)
- Billing service callback (MRR change)
- Feature flag toggle
1// Example: call group after a plan change
2analytics.group("ws_abc123", {
3 name: "Acme Corp Workspace",
4 plan: "pro_annual",
5 plan_name: "Pro Annual",
6 employees: 47,
7 mrr: 49900,
8 trial_status: "converted"
9});If your backend doesn't have convenient hooks for every field change, a common pattern is a nightly batch job that compares current values to a snapshot and fires group only for orgs where scoring-relevant fields differ.
Segment delivers this as:
1{
2 "type": "group",
3 "groupId": "ws_abc123",
4 "traits": {
5 "name": "Acme Corp Workspace",
6 "plan": "pro_annual",
7 "plan_name": "Pro Annual",
8 "employees": 47,
9 "mrr": 49900,
10 "trial_status": "converted"
11 },
12 "userId": "user_789",
13 "timestamp": "2026-01-15T10:30:00.000Z",
14 "messageId": "seg-msg-abc123"
15}Payload Template Mapping
Configure the payload template on your API key to match Segment's group call structure:
| TrailSpark Field | Payload Path |
|---|---|
| Product Org ID | groupId |
| Name | traits.name |
| Plan ID | traits.plan |
| Plan Name | traits.plan_name |
| User Count | traits.employees |
| MRR | traits.mrr |
| Trial Status | traits.trial_status |
Rudderstack
Rudderstack follows the same event specification as Segment. Use the Webhook destination with a transformation to filter for group calls.
Adding the Destination
- In the Rudderstack dashboard, go to Destinations > New Destination
- Select Webhook (or HTTP Webhook for the no-code option)
- Enter the webhook URL:
https://{subdomain}.trailspark.ai/api/product-orgs/webhook/{apiKey} - Set request format to JSON
- Add headers if your API key has a secret:
X-Api-Secret: {secret}
Adding a Transformation
Rudderstack's webhook destination sends all event types by default. Add a transformation to filter for group calls only:
- Go to Transformations > New Transformation
- Add the following code:
1export function transformEvent(event, metadata) {
2 if (event.type !== 'group') {
3 return null;
4 }
5 return event;
6}- Attach the transformation to the webhook destination connection
Instrumenting Group Calls
The same guidance applies as Segment — only fire group when scoring-relevant fields change, not on every request.
1rudderanalytics.group("ws_abc123", {
2 name: "Acme Corp Workspace",
3 plan: "pro_annual",
4 plan_name: "Pro Annual",
5 employees: 47,
6 mrr: 49900,
7 trial_status: "converted"
8});Rudderstack's group payload uses the same structure as Segment (groupId, traits.*), so the same payload template mapping works for both platforms. See the Segment mapping table above.
Hightouch
Hightouch is a reverse ETL tool that syncs data from your data warehouse to destinations. Instead of instrumenting group calls in your product code, you write a SQL model that selects org-level data and Hightouch sends it to TrailSpark on a schedule.
Creating the HTTP Request Destination
- In Hightouch, go to Destinations > Add Destination
- Select HTTP Request
- Set the Base URL:
https://{subdomain}.trailspark.ai - Add headers if your API key has a secret:
X-Api-Secret: {secret}
Creating the Source Model
Create a SQL model that selects the org-level data you want to sync.
Only include columns that matter for scoring. Hightouch CDC compares the full model output between syncs — if any column changes, that row is sent as a webhook call. Including volatile columns like last_active_at or updated_at causes every org to be sent on every sync, defeating the purpose of CDC.
- Go to Models > Add Model
- Select your data warehouse source and choose SQL editor
- Write a query that returns one row per product org, including only scoring-relevant fields:
1SELECT
2 workspace_id,
3 workspace_name,
4 plan_id,
5 plan_name,
6 user_count,
7 mrr_cents AS mrr,
8 trial_status
9FROM product.workspaces- Click Preview to verify results
- Set the Primary key to
workspace_id - Save the model
With this model, Hightouch only sends a webhook when plan_id, plan_name, user_count, mrr_cents, or trial_status actually changes for an org. Orgs with no changes are skipped entirely.
Creating the Sync
- Go to Syncs > Add Sync
- Select your SQL model as the source and the HTTP Request destination
- Configure request triggers: Rows added and Rows changed
- Set the endpoint path:
/api/product-orgs/webhook/{apiKey} - Set method to POST
- In the JSON editor, map columns using Liquid template syntax:
1{
2 "workspace_id": "{{row.workspace_id}}",
3 "workspace_name": "{{row.workspace_name}}",
4 "plan_id": "{{row.plan_id}}",
5 "plan_name": "{{row.plan_name}}",
6 "user_count": {{row.user_count}},
7 "mrr": {{row.mrr}},
8 "trial_status": "{{row.trial_status}}"
9}- Set the sync schedule — hourly or daily is typical
- Save and enable
Payload Template Mapping
Since the Hightouch payload uses a flat structure, the payload template paths are just the top-level field names:
| TrailSpark Field | Payload Path |
|---|---|
| Product Org ID | workspace_id |
| Name | workspace_name |
| Plan ID | plan_id |
| Plan Name | plan_name |
| User Count | user_count |
| MRR | mrr |
| Trial Status | trial_status |
Hightouch's change data capture (CDC) automatically compares model output between syncs. Only orgs where at least one column value changed will trigger a webhook call — unchanged orgs are skipped entirely.
When to Send Updates
Send a product org update only when a scoring-relevant attribute changes. Each webhook call is an API request — sending unchanged data wastes volume and provides no scoring value.
Attributes worth tracking:
- Plan changes — upgrades, downgrades, trial start or end
- User count — new users added or removed from the workspace
- MRR changes — billing adjustments, add-ons, renewals
- Feature flag toggles — SSO enabled, API access granted
- Trial status transitions — active → expired → converted
Attributes to avoid (change too frequently, low scoring value):
last_active_at/last_login_at— updates on every sessionupdated_at— updates on any row change, including non-scoring fields- Request counts or page views — better captured as behavioral signals via the signal staging endpoint
Platform-specific guidance
Segment and Rudderstack: Only fire group calls from code paths where scoring-relevant fields are being modified — subscription handlers, billing callbacks, user management endpoints. Do not call group on every page load or API request.
Hightouch: Only include scoring-relevant columns in your SQL model. CDC compares the full model output between syncs, so any column change triggers a send. If your model includes last_active_at, every active org gets sent on every sync. Remove volatile columns to let CDC do its job.
Testing
Send a test payload with cURL to verify your endpoint and payload template:
1curl -X POST \
2 https://{subdomain}.trailspark.ai/api/product-orgs/webhook/{apiKey} \
3 -H "Content-Type: application/json" \
4 -d '{
5 "workspace_id": "test_ws_001",
6 "workspace_name": "Test Workspace",
7 "plan_id": "pro",
8 "plan_name": "Pro",
9 "user_count": 10,
10 "mrr": 9900,
11 "trial_status": "active"
12 }'A successful response returns:
1{
2 "success": true,
3 "productOrgId": "test_ws_001",
4 "targetOrgId": 123,
5 "isNewOrg": true
6}Then check Settings > Org Management in TrailSpark to confirm the product org appeared with the correct fields.
Troubleshooting
HTTP 403 — The API key was not created with the Product Org Updates endpoint type. Create a new key with the correct type at Settings > API Keys.
HTTP 400: missing productOrgId — The payload template's Product Org ID path does not match a field in the incoming payload. Verify the dot-notation path (e.g., groupId for Segment, workspace_id for flat payloads).
Fields not appearing — Check that the optional field paths in the payload template match your actual payload structure. Paths are case-sensitive and use dot notation for nested fields (e.g., traits.plan).
Hightouch sync failing — Verify the primary key column contains unique, non-null values. Check that the SQL model returns results when previewed. Confirm the base URL and endpoint path are correct.
Duplicate product orgs — TrailSpark upserts by Product Org ID. If you see duplicates, verify the Product Org ID value is consistent across all payloads (case-sensitive exact match).
Next Steps
- Product Org Updates — full endpoint and payload template reference
- API Keys — manage keys and configure secrets
- Creating Signal Mapping — set up rules to score incoming signals
