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
- Create an API key at Settings > API Keys (see Managing API Keys)
- Configure your sending system with the webhook URL,
POSTmethod, andContent-Type: application/json - Test with a sample request:
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):
1{
2 "id": 123,
3 "message": "Signal received and queued for processing",
4 "source": "generic",
5 "cold_storage": false
6}Request Requirements
Required Headers
| Header | Value |
|---|---|
Content-Type | application/json |
Optional Headers
| Header | Description |
|---|---|
X-Api-Secret | Shared secret (required if secret is configured on the API key) |
X-Source | Explicitly set the signal source (overrides auto-detection) |
Response Codes
| Code | Meaning | Action |
|---|---|---|
| 202 | Accepted | Signal received and queued for processing |
| 400 | Bad Request | Check payload format / invalid JSON |
| 401 | Unauthorized | API key is invalid, inactive, expired, or missing required secret |
| 429 | Rate Limited | Request rate limit exceeded, or ingested signals have passed twice your plan's limit. Upgrade plan or enable overages |
| 500 | Server Error | Retry 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:
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:
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:
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:
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}