Webhooks
Trace fires an HTTP POST to your webhook endpoints whenever a key event occurs — installs, opens, events, and SKAdNetwork postbacks. Each app can have multiple webhooks, each filtering by event type. Webhooks are available on all plans, including the free tier.
Managing webhooks
Section titled “Managing webhooks”Create, list, update, and delete webhooks via the API or CLI. Each webhook has a URL, signing secret, optional event filter, and can be enabled/disabled independently.
Create a webhook
Section titled “Create a webhook”POST /v1/webhooks# Subscribe to all eventstrace webhooks create \ --url https://example.com/webhooks/trace \ --secret whsec_your_secret_here
# Subscribe to specific events onlytrace webhooks create \ --url https://example.com/webhooks/installs \ --secret whsec_installs_secret \ --events install.attributed,install.organiccurl -X POST https://api.traceclick.io/v1/webhooks \ -H "X-Api-Key: tr_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/trace", "secret": "whsec_your_secret_here", "events": [] }'| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Webhook endpoint URL |
secret | string | When no template | HMAC-SHA256 signing secret |
events | string[] | No | Event types to deliver (empty = all events) |
template | string | No | Message template: slack, discord, or teams |
templateConfig | object | No | Template options (see Templates) |
Response (201 Created):
{ "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://example.com/webhooks/trace", "events": [], "enabled": true, "template": null, "templateConfig": null, "createdAt": "2025-07-15T14:30:00.000+00:00"}List webhooks
Section titled “List webhooks”GET /v1/webhooksReturns all webhooks for the app. The secret field is never included in responses.
trace webhooks listcurl https://api.traceclick.io/v1/webhooks \ -H "X-Api-Key: tr_live_xxxxxxxxxxxx"Get a webhook
Section titled “Get a webhook”GET /v1/webhooks/{id}Update a webhook
Section titled “Update a webhook”PATCH /v1/webhooks/{id}# Change URLtrace webhooks update <id> --url https://example.com/new-hook
# Disable a webhooktrace webhooks update <id> --enabled=false
# Change event filtertrace webhooks update <id> --events install.attributed,open.attributedcurl -X PATCH https://api.traceclick.io/v1/webhooks/<id> \ -H "X-Api-Key: tr_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"enabled": false}'| Field | Type | Description |
|---|---|---|
url | string? | New endpoint URL |
secret | string? | New signing secret |
events | string[]? | New event filter (empty array = all events) |
enabled | boolean? | Enable or disable |
Delete a webhook
Section titled “Delete a webhook”DELETE /v1/webhooks/{id}trace webhooks delete <id>curl -X DELETE https://api.traceclick.io/v1/webhooks/<id> \ -H "X-Api-Key: tr_live_xxxxxxxxxxxx"Returns 204 No Content on success.
Envelope format
Section titled “Envelope format”Every webhook POST has a JSON body with this structure:
{ "event": "install.attributed", "timestamp": "2025-07-15T14:30:00.000+00:00", "data": { // event-specific fields (see below) }}Headers
Section titled “Headers”| Header | Description |
|---|---|
Content-Type | application/json |
X-Trace-Event | Event type (e.g. install.attributed) |
X-Trace-Signature | HMAC-SHA256 hex digest of the raw body, signed with your webhook secret |
Signature verification
Section titled “Signature verification”Verify the X-Trace-Signature header to confirm the request came from Trace. Each webhook has its own signing secret.
import crypto from 'node:crypto';
function verifySignature(body, secret, signature) { const expected = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) );}
// In your handler:const body = await request.text();const sig = request.headers.get('X-Trace-Signature');if (!verifySignature(body, process.env.WEBHOOK_SECRET, sig)) { return new Response('Invalid signature', { status: 401 });}const event = JSON.parse(body);import hmac, hashlib
def verify_signature(body: bytes, secret: str, signature: str) -> bool: expected = hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)
# In your handler:body = await request.body()sig = request.headers["X-Trace-Signature"]if not verify_signature(body, WEBHOOK_SECRET, sig): return Response("Invalid signature", status_code=401)event = json.loads(body)Retry behavior
Section titled “Retry behavior”| Property | Value |
|---|---|
| Timeout | 10 seconds per attempt |
| Max attempts | 3 |
| Backoff | 1s, 5s, 25s (exponential) |
| Success | Any 2xx status code |
If all 3 attempts fail, the failure is recorded in the audit log. Every attempt (success or failure) is recorded in the delivery log, queryable via the delivery history API.
Event types
Section titled “Event types”install.organic
Section titled “install.organic”Fired when an install is recorded with no matching click (organic).
{ "event": "install.organic", "timestamp": "2025-07-15T14:30:00.000+00:00", "data": { "installId": "550e8400-e29b-41d4-a716-446655440000", "platform": "ANDROID", "appName": "My App", "deviceModel": "Pixel 8", "osVersion": "14", "locale": "en-US" }}| Field | Type | Description |
|---|---|---|
installId | string | UUID of the install record |
platform | string | ANDROID or IOS |
appName | string | App display name |
deviceModel | string? | Device model (e.g. Pixel 8, iPhone15,2) |
osVersion | string? | OS version string |
locale | string? | Device locale (e.g. en-US) |
install.attributed
Section titled “install.attributed”Fired when an install is matched to a click.
{ "event": "install.attributed", "timestamp": "2025-07-15T14:30:00.000+00:00", "data": { "installId": "550e8400-e29b-41d4-a716-446655440000", "method": "FINGERPRINT", "campaignId": "summer_sale", "platform": "IOS", "appName": "My App", "shortCode": "abc123", "deepLinkPath": "/product/123", "confidence": "0.87", "clickedAt": "2025-07-15T14:25:00.000+00:00" }}| Field | Type | Description |
|---|---|---|
installId | string | UUID of the install record |
method | string | Attribution method (CLICK_ID, FINGERPRINT, INSTALL_REFERRER) |
campaignId | string? | Campaign ID from the matched click (null if none) |
platform | string | ANDROID or IOS |
appName | string | App display name |
shortCode | string? | Short link code that was clicked |
deepLinkPath | string? | Deep link path from the matched click |
confidence | string? | Fingerprint match confidence score |
clickedAt | string? | When the matched click occurred |
open.attributed
Section titled “open.attributed”Fired when a deferred deep link open is matched to a click.
{ "event": "open.attributed", "timestamp": "2025-07-15T14:30:00.000+00:00", "data": { "openId": "550e8400-e29b-41d4-a716-446655440000", "method": "CLICK_ID", "campaignId": "summer_sale", "deepLinkPath": "/product/123", "appName": "My App", "shortCode": "abc123", "deepLinkParams": "{\"ref\":\"hero\"}", "confidence": null }}| Field | Type | Description |
|---|---|---|
openId | string | UUID of the open event record |
method | string | Attribution method (CLICK_ID or FINGERPRINT) |
campaignId | string? | Campaign ID from the matched click |
deepLinkPath | string? | Deep link path from the matched click |
appName | string | App display name |
shortCode | string? | Short link code that was clicked |
deepLinkParams | string? | Deep link parameters JSON |
confidence | string? | Fingerprint match score (null for CLICK_ID) |
event.recorded
Section titled “event.recorded”Fired when a post-install event is recorded.
{ "event": "event.recorded", "timestamp": "2025-07-15T14:30:00.000+00:00", "data": { "eventId": "550e8400-e29b-41d4-a716-446655440000", "installId": "660e8400-e29b-41d4-a716-446655440000", "eventName": "purchase", "properties": "{\"amount\": \"9.99\", \"currency\": \"USD\"}", "appName": "My App", "platform": "android", "userId": "user_abc123" }}| Field | Type | Description |
|---|---|---|
eventId | string | UUID of the event record |
installId | string | UUID of the associated install |
eventName | string | Name of the event |
properties | string | JSON-encoded key-value properties |
appName | string | App display name |
platform | string | Install platform |
userId | string? | Linked user ID (if identify() was called) |
skan.postback_received
Section titled “skan.postback_received”Fired when an Apple SKAdNetwork postback is received.
{ "event": "skan.postback_received", "timestamp": "2025-07-15T14:30:00.000+00:00", "data": { "appName": "My App", "version": "4.0", "ad-network-id": "example.skadnetwork", "campaign-id": "42", "transaction-id": "a1b2c3d4", "source-app-id": "123456789", "fidelity-type": 1, "did-win": true, "conversion-value": 63, "postback-sequence-index": 0 }}The data field contains the raw Apple postback object with appName prepended.
test.webhook
Section titled “test.webhook”Fired when you use the test endpoint. Useful for verifying your integration.
{ "event": "test.webhook", "timestamp": "2025-07-15T14:30:00.000+00:00", "data": { "message": "Test webhook from Trace", "appId": "550e8400-e29b-41d4-a716-446655440000", "webhookId": "660e8400-e29b-41d4-a716-446655440000", "appName": "My App" }}Templates
Section titled “Templates”Templates transform webhook payloads into platform-native rich messages. When a template is set, the raw JSON envelope is replaced with the platform’s native message format (Slack Block Kit, Discord Embeds, or Teams Adaptive Cards). HMAC signing is skipped for templated webhooks.
Available templates
Section titled “Available templates”| ID | Platform | Description |
|---|---|---|
slack | Slack | Block Kit messages with color-coded sidebars |
discord | Discord | Rich embeds with color-coded sidebars |
teams | Microsoft Teams | Adaptive Card v1.4 messages |
List templates
Section titled “List templates”GET /v1/webhooks/templatesReturns the list of available templates:
[ {"id": "slack", "name": "Slack", "description": "Rich messages using Slack Block Kit with color-coded sidebars"}, {"id": "discord", "name": "Discord", "description": "Rich embeds with color-coded sidebars for Discord webhooks"}, {"id": "teams", "name": "Microsoft Teams", "description": "Adaptive Card messages for Microsoft Teams incoming webhooks"}]Creating a templated webhook
Section titled “Creating a templated webhook”When using a template, secret is not required:
# Slack webhooktrace webhooks create \ --url https://hooks.slack.com/services/T.../B.../xxx \ --template slack
# Discord webhooktrace webhooks create \ --url https://discord.com/api/webhooks/123/abc \ --template discordcurl -X POST https://api.traceclick.io/v1/webhooks \ -H "X-Api-Key: tr_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.slack.com/services/T.../B.../xxx", "template": "slack", "events": ["install.attributed"] }'Template config
Section titled “Template config”Optional templateConfig object for customization:
| Field | Type | Description |
|---|---|---|
mentionText | string? | Mention/ping text (e.g. @channel for Slack, @everyone for Discord) |
accentColor | string? | Override the default event color (hex, e.g. #ff0000) |
curl -X POST https://api.traceclick.io/v1/webhooks \ -H "X-Api-Key: tr_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.slack.com/services/T.../B.../xxx", "template": "slack", "templateConfig": {"mentionText": "@channel"} }'Color coding
Section titled “Color coding”Each event type has a default accent color used for sidebars/embeds:
| Event | Color |
|---|---|
install.attributed | Green (#16a34a) |
install.organic | Gray (#6b7280) |
open.attributed | Blue (#2563eb) |
event.recorded | Purple (#9333ea) |
skan.postback_received | Amber (#f59e0b) |
test.webhook | Slate (#64748b) |
Test webhook
Section titled “Test webhook”POST /v1/webhooks/{id}/testSend a test.webhook event to a specific webhook endpoint. The request is delivered synchronously so you get immediate feedback.
Requires: Secret key (tr_live_*)
Response
Section titled “Response”{ "success": true}Returns 200 on success, 502 if delivery failed, or 404 if webhook not found.
Example
Section titled “Example”trace webhooks test <webhook-id>curl -X POST https://api.traceclick.io/v1/webhooks/<webhook-id>/test \ -H "X-Api-Key: tr_live_xxxxxxxxxxxx"Delivery history
Section titled “Delivery history”GET /v1/webhooks/deliveriesQuery recent webhook delivery attempts for your app.
Requires: Secret key (tr_live_*)
Query parameters
Section titled “Query parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 25 | Max results (1–100) |
event_type | string | — | Filter by event type (e.g. install.attributed) |
webhook_id | string | — | Filter by webhook ID |
Response
Section titled “Response”[ { "id": "550e8400-e29b-41d4-a716-446655440000", "app_id": "660e8400-e29b-41d4-a716-446655440000", "webhook_id": "770e8400-e29b-41d4-a716-446655440000", "event_type": "install.attributed", "webhook_url": "https://example.com/webhooks/trace", "request_body": "{\"event\":\"install.attributed\",\"timestamp\":\"...\",\"data\":{...}}", "http_status": 200, "response_body": "OK", "error_message": null, "attempt_number": 1, "success": true, "duration_ms": 142, "created_at": "2025-07-15T14:30:00.000+00:00" }]Example
Section titled “Example”# Recent deliveriestrace webhooks deliveries --limit 10
# Filter by event typetrace webhooks deliveries --event-type install.attributed
# Filter by webhook IDtrace webhooks deliveries --webhook-id <webhook-id># Recent deliveriescurl "https://api.traceclick.io/v1/webhooks/deliveries?limit=10" \ -H "X-Api-Key: tr_live_xxxxxxxxxxxx"
# Filter by webhook IDcurl "https://api.traceclick.io/v1/webhooks/deliveries?webhook_id=<id>" \ -H "X-Api-Key: tr_live_xxxxxxxxxxxx"