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.

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
1curl -X POST \
2  "https://app.trailspark.ai/api/signal-staging/webhook/YOUR_API_KEY" \
3  -H "Content-Type: application/json" \
4  -d '{"email": "test@example.com", "event": "test"}'

Expected response (HTTP 202):

json
1{
2  "id": 123,
3  "message": "Signal received and queued for processing",
4  "source": "generic",
5  "cold_storage": false
6}

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
1{
2  "signals": [
3    {"email": "lead1@example.com", "event": "page_view", "properties": {"page": "/pricing"}},
4    {"email": "lead2@example.com", "event": "form_submission", "properties": {"form": "contact"}}
5  ]
6}

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
1const axios = require('axios');
2
3await axios.post(
4  'https://app.trailspark.ai/api/signal-staging/webhook/YOUR_API_KEY',
5  { email, event, properties }
6);

Python:

python
1import requests
2
3requests.post(
4    'https://app.trailspark.ai/api/signal-staging/webhook/YOUR_API_KEY',
5    json={'email': email, 'event': event, 'properties': properties}
6)

Retry Strategy

Implement exponential backoff for 5xx errors:

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

Next Steps