> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tamradar.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Payloads

> Webhook delivery model, payload envelope, update types, and testing.

## How TAMradar delivers updates

TAMradar supports two ways to receive radar findings:

* **Webhooks (push)** — TAMradar POSTs events to your `webhook_url` in real-time. Set `webhook_url` when creating a radar to enable this.
* **Polling (pull)** — Fetch findings on demand via `GET /v1/updates`. Works for all radars, including those created without a `webhook_url`.

Both delivery methods use the same payload format documented below.

## Payload Libraries by Category

### Company Payloads

* [Company Mentions](/webhooks/company/mentions)
* [Company Social Posts](/webhooks/company/social-posts)
* [Company Social Posts (CXO)](/webhooks/company/social-posts-cxo)
* [Company Social Engagements](/webhooks/company/social-engagements)
* [Company Job Openings](/webhooks/company/job-openings)
* [Company New Hires](/webhooks/company/new-hires)
* [Company Promotions](/webhooks/company/promotions)
* [Company Reviews](/webhooks/company/reviews)

### Contact Payloads

* [Contact Job Changes](/webhooks/contact/job-changes)
* [Contact Social Posts](/webhooks/contact/social-posts)
* [Contact Social Engagements](/webhooks/contact/social-engagements)

### Industry Payloads

* [Industry Mentions](/webhooks/industry/mentions)
* [Industry Funding Rounds](/webhooks/industry/funding-rounds)
* [Industry New Companies](/webhooks/industry/new-companies)
* [Industry Job Openings](/webhooks/industry/job-openings)

## Setting Up Webhook Delivery (optional)

If you want real-time push delivery, set up an HTTPS endpoint:

1. Create an HTTPS endpoint on your server that can receive POST requests
2. Pass `webhook_url` when creating your radar
3. Ensure your endpoint returns a 2xx status code to acknowledge receipt

> **No webhook?** Skip `webhook_url` entirely to create a poll-only radar. Findings are always available via `GET /v1/updates` regardless.

## Webhook Delivery & Reliability

### Timeouts

TAMradar waits **10 seconds** for your endpoint to respond. If no response is received within that window, the attempt is treated as a failure and retried.

### Retries & backoff

If your endpoint returns a non-2xx status (including 5xx) or times out, TAMradar retries up to **3 times** with exponential backoff:

| Attempt | Delay before retry |
| - | - |
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 15 minutes |

After 4 total attempts (1 initial + 3 retries), delivery is permanently abandoned.

### Data is never lost

Push delivery failure does not mean data loss. Every event is written to our internal store before delivery is attempted — findings are always retrievable via `GET /v1/updates`, regardless of webhook delivery outcome. Use polling as a fallback if your endpoint experiences downtime.

### Deduplication

On retry, the same `update_id` is resent. Use `update_id` as your idempotency key to deduplicate events safely.

## Webhook Payload Format

All webhook payloads follow this structure:

```json theme={null}
{
  "update_id": "550e8400-e29b-41d4-a716-446655440000",
  "update_type": "radar_finding",
  "record_id": "c4d5e6f7-a8b9-4c0d-b1e2-f3a4b5c6d7e8",

  "discovered_at": "2024-03-20T12:30:00Z",
  "data": {
    "radar_id": "54e32291-25a4-459f-8bd6-4f79489ea931",
    "radar_type": "company_job_openings",
    "domain": "example.com",
    "custom_fields": {
      "priority": "high",
      "target_account_id": "0011U00000TFV7MQAX"
    }
  },
  "content": {
    // Event-specific data
  }
}
```

### Base Fields

| Field | Type | Required | Description |
| - | - | - | - |
| update\_id | string | Yes | Unique identifier for this webhook update |
| update\_type | string | Yes | Type of update (see Update Types below) |
| record\_id | string | No | Unique identifier for the detected record |
| radar\_type | string | Yes | Type of radar that generated the event |
| discovered\_at | string | Yes | ISO timestamp (UTC) when the update was detected |

### Update Types

| Update Type | Description |
| - | - |
| `radar_finding` | New data detected by a radar (default) |
| `radar_failure` | Radar setup or billing failure (may include refund metadata) |
| `radar_created` | Async bulk item was created successfully |
| `bulk_completed` | Async bulk request finished processing all items |

### Per-item bulk webhook events

Before the final `bulk_completed`, each item delivers its own webhook as it finishes:

* **`radar_created`** — item was created successfully. Includes `code: 201`, `message: "Radar created successfully"`, `completed_at`, and a full `data` block matching the single-radar creation response.
* **`radar_failure`** — item failed validation, conflict, or billing. Includes `completed_at`, `code`, `errors[]`, and `message` matching the sync API error shape. Also emits `discovered_at` (same value as `completed_at`) for backwards compatibility — prefer `completed_at` going forward.

`radar_created` example:

```json theme={null}
{
  "update_id": "efbf7c77-c1ae-4a0d-85b6-116674e9e0b1",
  "update_type": "radar_created",
  "record_id": null,
  "code": 201,
  "message": "Radar created successfully",
  "completed_at": "2026-05-25T11:17:20Z",
  "bulk_id": "5f9da22e-4778-427c-b7e4-dab2a8d9d709",
  "item_index": 0,
  "custom_fields": { "crm_account_id": "acc_001" },
  "status": "success",
  "data": {
    "radar_id": "2ffb1a07-ac95-4a6d-bc34-39f479f1c856",
    "radar_type": "company_job_openings",
    "domain": "stripe.com",
    "radar_status": "active",
    "webhook_url": "https://your-domain.com/webhooks/tamradar",
    "created_at": "2026-05-25T11:17:18Z",
    "updates_since": null,
    "deactivated_at": null,
    "next_charge_at": "2026-06-25T11:17:18Z",
    "custom_fields": { "crm_account_id": "acc_001" }
  }
}
```

`radar_failure` example:

```json theme={null}
{
  "update_id": "a1b2c3d4-0001-4000-b000-000000000001",
  "update_type": "radar_failure",
  "record_id": null,
  "completed_at": "2026-05-25T11:17:26Z",
  "discovered_at": "2026-05-25T11:17:26Z",
  "bulk_id": "5f9da22e-4778-427c-b7e4-dab2a8d9d709",
  "item_index": 1,
  "custom_fields": {},
  "status": "error",
  "code": 409,
  "message": "Conflict: Radar already exists",
  "errors": [
    {
      "field": "domain",
      "reason": "A radar with this domain and type already exists for your account. Conflicting radar_id: 3f1d8e8a-39c4-47e8-a4b4-f7db6a2e50f0. You can view it using GET /radars/{radar_id}",
      "conflicting_radar_id": "3f1d8e8a-39c4-47e8-a4b4-f7db6a2e50f0"
    }
  ],
  "refund_amount_usd": 0,
  "error_id": "45ed35e5-986c-4496-afd8-9a84f6baa410",
  "timestamp": "2026-05-25T11:17:26.000Z"
}
```

### Bulk `update_id` semantics

For `bulk_completed` events, two ID levels can appear and they serve different purposes:

* Top-level `update_id`: idempotency key for the whole `bulk_completed` webhook event.
* Per-item `radars[].update_id`: correlation key for each item's earlier `radar_created` or `radar_failure` event.

Store both when you consume bulk webhooks:

* Deduplicate `bulk_completed` retries by top-level `update_id`.
* Correlate each `radars[]` item back to its individual item event with `radars[].update_id`.

`bulk_completed` payloads use a batch envelope with `completed_at`, `summary`, and `radars[]`. Each entry in `radars[]` is the verbatim payload from that item's per-item webhook:

```json theme={null}
{
  "update_id": "9a2f0f77-5c8b-4b3e-b636-3ad539f4de65",
  "update_type": "bulk_completed",
  "bulk_id": "5f9da22e-4778-427c-b7e4-dab2a8d9d709",
  "completed_at": "2026-05-25T11:17:33Z",
  "summary": {
    "total": 3,
    "created": 2,
    "failed": 1
  },
  "radars": [
    {
      "update_id": "efbf7c77-c1ae-4a0d-85b6-116674e9e0b1",
      "update_type": "radar_created",
      "code": 201,
      "message": "Radar created successfully",
      "completed_at": "2026-05-25T11:17:20Z",
      "bulk_id": "5f9da22e-4778-427c-b7e4-dab2a8d9d709",
      "item_index": 0,
      "custom_fields": { "crm_account_id": "acc_001" },
      "status": "success",
      "data": { "radar_id": "2ffb1a07-ac95-4a6d-bc34-39f479f1c856" }
    },
    {
      "update_id": "a1b2c3d4-0001-4000-b000-000000000001",
      "update_type": "radar_failure",
      "record_id": null,
      "completed_at": "2026-05-25T11:17:26Z",
      "discovered_at": "2026-05-25T11:17:26Z",
      "bulk_id": "5f9da22e-4778-427c-b7e4-dab2a8d9d709",
      "item_index": 1,
      "custom_fields": {},
      "status": "error",
      "code": 409,
      "message": "Conflict: Radar already exists",
      "errors": [
        {
          "field": "domain",
          "reason": "A radar with this domain and type already exists for your account. Conflicting radar_id: 3f1d8e8a-39c4-47e8-a4b4-f7db6a2e50f0.",
          "conflicting_radar_id": "3f1d8e8a-39c4-47e8-a4b4-f7db6a2e50f0"
        }
      ],
      "refund_amount_usd": 0,
      "error_id": "45ed35e5-986c-4496-afd8-9a84f6baa410",
      "timestamp": "2026-05-25T11:17:26Z"
    }
  ]
}
```

### Radar Types

| Radar Type | Description |
| - | - |
| `company_mentions` | Social media and article mentions of your company |
| `company_social_posts` | Company's own social media activity |
| `company_social_posts_cxo` | Social media posts from company executives |
| `company_social_engagements` | Social media engagements (likes, comments) on company posts |
| `company_job_openings` | New job postings from tracked companies |
| `company_new_hires` | New employee detections |
| `company_promotions` | Employee promotions and role changes |
| `company_reviews` | Customer reviews across platforms |
| `contact_job_changes` | Individual contact career changes (departures, promotions, role changes) |
| `contact_social_posts` | Social media posts from tracked contacts |
| `contact_social_engagements` | Social media engagements (likes, comments) by tracked contacts |
| `industry_mentions` | Industry-wide mentions and news articles |
| `industry_funding_rounds` | Funding rounds across companies in your industry |
| `industry_job_openings` | Job openings across companies in your industry |
| `industry_new_companies` | New companies emerging in your industry |

## Radar Failure Notifications

When a radar fails setup or is deactivated due to billing, you'll receive a `radar_failure` notification:

```json theme={null}
{
  "update_id": "550e8400-e29b-41d4-a716-446655440000",
  "update_type": "radar_failure",
  "record_id": null,
  "discovered_at": "2024-03-20T12:30:00Z",
  "completed_at": "2024-03-20T12:30:00Z",
  "data": {
    "radar_id": "54e32291-25a4-459f-8bd6-4f79489ea931",
    "radar_type": "company_job_openings",
    "domain": "example.com",
    "custom_fields": {
      "priority": "high"
    }
  },
  "status": "error",
  "code": 402,
  "message": "Radar was deactivated due to insufficient credits.",
  "errors": [{ "field": "radar", "reason": "insufficient_funds" }],
  "refund_amount_usd": 0,
  "timestamp": "2024-03-20T12:30:00Z",
  "error_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

When setup fails, the same `radar_failure` shape is used with a different `code` and refund value:

```json theme={null}
{
  "update_id": "550e8400-e29b-41d4-a716-446655440001",
  "update_type": "radar_failure",
  "record_id": null,
  "discovered_at": "2024-03-20T12:30:00Z",
  "completed_at": "2024-03-20T12:30:00Z",
  "data": {
    "radar_id": "54e32291-25a4-459f-8bd6-4f79489ea931",
    "radar_type": "company_social_posts",
    "domain": "example.com",
    "custom_fields": {
      "priority": "high"
    }
  },
  "status": "error",
  "code": 400,
  "message": "Radar creation failed because the domain does not have required social media accounts.",
  "errors": [{ "field": "radar", "reason": "missing_prerequisites" }],
  "refund_amount_usd": 0.10,
  "timestamp": "2024-03-20T12:30:00Z",
  "error_id": "550e8400-e29b-41d4-a716-446655440001"
}
```

### Failure Reasons

Identify the failure type from `code` and `message`. The `errors[].reason` field contains the machine-readable reason key.

| `code` | `errors[].reason` | Description |
| - | - | - |
| `400` | `missing_prerequisites` | Required accounts or data sources not found |
| `400` | `setup_failure` | General setup failure during radar initialization |
| `400` | `source_inaccessible` | Target source (e.g. LinkedIn profile, domain) is inaccessible or private |
| `402` | `insufficient_funds` | Radar deactivated — balance too low to cover the charge |

## Best Practices for Webhook Handling

1. **Respond Quickly**: Return a 2xx status code immediately, then process asynchronously
2. **Implement Retry Logic**: Be prepared to handle the same event multiple times
3. **Verify Signatures**: Check webhook signatures to validate authenticity (documentation coming soon)
4. **Track Failure Events**: Monitor `radar_failure` to handle billing/setup problems quickly
5. **Handle Null Fields**: Some fields like `record_id` may be null for certain event types

## Testing Your Webhook

### Quick test

Trigger a sample payload to your endpoint:

```bash theme={null}
curl -X POST https://api.tamradar.com/v1/webhooks/test \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://your-domain.com/webhook",
    "radar_type": "company_job_openings"    // any supported radar_type
    // "update_type": "radar_failure"        // optional, defaults to radar_finding
  }'
```

The test event is fire-and-forget, with no retries or signatures, and uses the same payload format you'll receive in production.

Need help? [Contact our team](mailto:support@tamradar.com)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.