Skip to main content
TrailsparkDocs

Configuring Webhooks

Webhook Endpoints

Trailspark provides several endpoint formats for signal ingestion:

Universal Webhook

POST https://app.trailspark.ai/api/signal-staging/webhook/{apiKey}

Accepts signals from any source. Source identification comes from the payload.

Source-Specific Webhook

POST https://app.trailspark.ai/api/signal-staging/webhook/{source}/{apiKey}

Include a source identifier in the URL (e.g., website, forms, marketing, custom). Useful when multiple systems send to the same key and you want to filter by source.

Source Presets on the Universal Webhook

The universal webhook also accepts a ?source= query parameter, which is the easiest way to label a webhook without switching to the source-specific URL format:

POST https://app.trailspark.ai/api/signal-staging/webhook/{apiKey}?source=heap

Settings > API Keys shows a Source picker next to each webhook key's URL — choose Generic, Segment, Heap, or Marketo and the shown URL updates automatically (adding ?source= for the sources that need it). A Payload template underneath the URL gives you a starting JSON body with the fields that source typically sends. Segment and Marketo payloads are usually detected automatically from their shape, but adding ?source= makes the label explicit regardless of payload variant.

Source resolution, in order: an X-Source header, then ?source= on the URL, then automatic detection from the payload shape, then a top-level source field in the payload body, and finally generic if none of those apply.

For Heap specifically, see Connecting Heap.

Batch Webhook

POST https://app.trailspark.ai/api/signal-staging/webhook/{apiKey}/batch

Send an array of signals in a single request.

Bulk Ingestion

POST https://app.trailspark.ai/api/signal-staging/bulk/{apiKey}

For historical data imports and high-volume batch processing.

Setup

  1. Create an API key at Settings > API Keys (see Managing API Keys)
  2. Configure your sending system with the webhook URL, POST method, and Content-Type: application/json
  3. Test with a sample request:
bash
curl -X POST \
  "https://app.trailspark.ai/api/signal-staging/webhook/YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "event": "test"}'

Expected response (HTTP 202):

json
{
  "id": 123,
  "message": "Signal received and queued for processing",
  "source": "generic",
  "cold_storage": false
}

Request Requirements

Required Headers

HeaderValue
Content-Typeapplication/json

Optional Headers

HeaderDescription
X-Api-SecretShared secret (required if secret is configured on the API key)
X-SourceExplicitly set the signal source (overrides auto-detection)

Response Codes

CodeMeaningAction
202AcceptedSignal received and queued for processing
400Bad RequestCheck payload format / invalid JSON
401UnauthorizedAPI key is invalid, inactive, expired, or missing required secret
429Rate LimitedRequest rate limit exceeded, or ingested signals have passed twice your plan's limit. Upgrade plan or enable overages
500Server ErrorRetry with exponential backoff

Rate Limits

Request rate limits are set per organization. When you exceed yours, requests receive 429 status codes.

For high-volume use, prefer the batch (/webhook/{apiKey}/batch) or bulk (/bulk/{apiKey}) endpoint to reduce request count.

Passing your plan's ingested-signal limit does not by itself cause a 429. Trailspark keeps accepting signals at no extra charge up to twice the limit and holds them — paused signals don't update your scores, and they're deleted when the billing period ends. Enable usage overages or upgrade and they resume immediately. Only beyond twice the limit are new signals rejected with a 429, until the next billing period.

That free extra room isn't permanent. On a paid plan, if your workspace ends three billing periods in a row over the allowance without pay-as-you-go turned on, the extra room is removed for good — after that, signals past the allowance are rejected with a 429 straight away instead of being collected and held. A period back inside the allowance resets the count, and pay-as-you-go remains available either way.

Bulk Payload Format

Wrap multiple signals in a signals array:

json
{
  "signals": [
    {"email": "lead1@example.com", "event": "page_view", "properties": {"page": "/pricing"}},
    {"email": "lead2@example.com", "event": "form_submission", "properties": {"form": "contact"}}
  ]
}

Each signal in the array is processed individually, with separate cold storage routing and usage tracking per signal.

Server-Side Integration Examples

Node.js:

javascript
const axios = require('axios');

await axios.post(
  'https://app.trailspark.ai/api/signal-staging/webhook/YOUR_API_KEY',
  { email, event, properties }
);

Python:

python
import requests

requests.post(
    'https://app.trailspark.ai/api/signal-staging/webhook/YOUR_API_KEY',
    json={'email': email, 'event': event, 'properties': properties}
)

Retry Strategy

Implement exponential backoff for 5xx errors:

javascript
async function sendWithRetry(url, payload, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
      });
      if (response.ok) return response;
      if (response.status < 500) throw new Error(`Client error: ${response.status}`);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
    }
  }
}

Checking That Events Are Landing

After you configure a webhook, open Settings > API Keys and expand Recent events on that key's card. Trailspark checks for new events every few seconds and shows the most recent ones — including the event name, who it came from, and the properties it carried — once they arrive. If nothing shows up after a few minutes, double-check the URL (including the ?source= parameter, if you set one) and that your sending system received a 202 response.

Each recent event also has a Create a mapping rule from this event link, which jumps straight to Signal Mapping with that event pre-filled.

Next Steps