# Configuring Webhooks

> Source: https://docs.trailspark.ai/webhooks-api/webhook-configuration

## 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](https://docs.trailspark.ai/crm-integration/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](https://docs.trailspark.ai/webhooks-api/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

| 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:

```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

- [Webhook Payload Format](https://docs.trailspark.ai/webhooks-api/webhook-payload-format)
- [API Keys](https://docs.trailspark.ai/webhooks-api/api-keys)
- [Creating Signal Mapping](https://docs.trailspark.ai/signal-management/creating-signal-mapping)
- [Connecting Heap](https://docs.trailspark.ai/crm-integration/connecting-heap)
