> ## 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.

# Bulk Create Guide

> End-to-end setup for asynchronous bulk radar creation and result handling.

Use this guide when you want to create many radars at once using `POST /v1/radars/bulk`.

Bulk creation is asynchronous:

* The submission request returns `202 Accepted` with a `bulk_id`.
* Item-level outcomes are produced later as `radar_created` or `radar_failure`.
* A final `bulk_completed` event indicates that all items are done.

For request and response schemas, use:

* [Bulk Create Radars](/api-reference/post-radars-bulk)
* [Get Bulk Create Status](/api-reference/get-radars-bulk-status)

## Bulk flow and responsibilities

| Component                       | Responsibility                                                             |
| ------------------------------- | -------------------------------------------------------------------------- |
| Your application                | Build the bulk payload, submit it, store `bulk_id`, reconcile outcomes     |
| `POST /v1/radars/bulk`          | Validates envelope-level shape and queues items                            |
| Bulk processor                  | Validates and processes each queued item asynchronously                    |
| Your webhook endpoint           | Receives per-item `radar_created` / `radar_failure`, then `bulk_completed` |
| `GET /v1/radars/bulk/{bulk_id}` | Polls live status and per-item results                                     |

## Step 1: Prepare your webhook consumer

`webhook_url` is required for async bulk. Point it to an HTTPS endpoint that:

* accepts `POST`
* returns `2xx` quickly
* processes payloads asynchronously in your own worker queue

You should handle these update types:

* `radar_created`
* `radar_failure`
* `bulk_completed`

See [Webhook Payloads](/webhooks/overview) for payload details and examples.

## Step 2: Build a valid bulk request

The top-level body requires:

* `webhook_url`: single callback URL for all items in this bulk submission
* `radars`: array of radar payloads

<Note>
  Each item in `radars[]` uses the same field structure as the corresponding single-radar endpoint. Before building a bulk payload, make sure you're familiar with the per-type schemas:

  * [Create Company Radar](/api-reference/post-radars-companies) — `domain`, `radar_type`, optional filters
  * [Create Contact Radar](/api-reference/post-radars-contacts) — `profile_url`, `radar_type`, `domain`
  * [Create Industry Radar](/api-reference/post-radars-industry) — `radar_type`, optional `keyword`
</Note>

We recommend adding `custom_fields` to every item. If you skip it, you can still reconcile with `item_index`, but item-level tracing in downstream systems is harder.

```json theme={null}
{
  "webhook_url": "https://your-domain.com/webhooks/tamradar",
  "radars": [
    {
      "radar_type": "company_job_openings",
      "domain": "stripe.com",
      "custom_fields": { "crm_account_id": "acc_001" }
    },
    {
      "radar_type": "contact_job_changes",
      "domain": "openai.com",
      "profile_url": "https://www.linkedin.com/in/example",
      "custom_fields": { "owner": "sdr_team" }
    }
  ]
}
```

## Step 3: Submit and persist `bulk_id`

On success, the API returns `202` with a `bulk_id` and summary. Persist `bulk_id` immediately. It is your primary key for reconciliation and polling.

```bash theme={null}
curl -X POST https://api.tamradar.com/v1/radars/bulk \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://your-domain.com/webhooks/tamradar",
    "radars": [
      {
        "radar_type": "company_job_openings",
        "domain": "stripe.com",
        "custom_fields": { "crm_account_id": "acc_001" }
      },
      {
        "radar_type": "industry_funding_rounds",
        "custom_fields": { "segment": "fintech" }
      }
    ]
  }'
```

```json theme={null}
{
  "status": "success",
  "code": 202,
  "message": "Bulk submission accepted - 2 radars queued for processing. Poll GET /v1/radars/bulk/5f9da22e-4778-427c-b7e4-dab2a8d9d709 for status or wait for the bulk_completed webhook.",
  "bulk_id": "5f9da22e-4778-427c-b7e4-dab2a8d9d709",
  "summary": {
    "total": 2
  },
  "timestamp": "2026-05-25T11:17:12Z"
}
```

Typical immediate failure cases:

* `400`: invalid JSON or invalid envelope (`webhook_url`, `radars[]`)
* `401`: missing or invalid API key
* `429`: bulk rate limit exceeded
* `500`: queueing failed

For error format details, see [Error Handling](/guides/error-handling).

`400` example:

```json theme={null}
{
  "status": "error",
  "code": 400,
  "message": "Invalid request body",
  "errors": [
    {
      "field": "webhook_url",
      "reason": "Invalid webhook URL format"
    }
  ],
  "timestamp": "2026-05-25T11:18:10Z",
  "error_id": "245df4c2-3ab8-4c96-a60d-42f6fc59f9b8"
}
```

`429` example:

```json theme={null}
{
  "status": "error",
  "code": 429,
  "message": "Rate limit exceeded — wait and retry.",
  "errors": [
    {
      "field": "rate_limit",
      "reason": "minute"
    }
  ],
  "timestamp": "2026-05-25T11:18:44Z",
  "error_id": "29a4ff58-f4a8-4ebe-8b2d-1544ee43a99c"
}
```

For async bulk, `reason` is always `minute` because this endpoint enforces a dedicated per-minute bulk bucket.

## Step 4: Process per-item events

Items do not complete at the same time. You can receive outcomes in any order.

`radar_created` means the item created successfully.
`radar_failure` means that item failed (validation, conflict, billing, or processing error). The `code`, `message`, and `errors[]` fields are identical to what the sync single-create endpoint would return for the same failure — so any error handling you already have for the sync API works here without changes.

Use `item_index`, `bulk_id`, and `custom_fields` to map each event back to the original request item.
For successful items, `data` matches the same `data` shape returned by single-create endpoints:
[Create Company Radar](/api-reference/post-radars-companies), [Create Contact Radar](/api-reference/post-radars-contacts), and [Create Industry Radar](/api-reference/post-radars-industry).

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

`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": { "segment": "fintech" },
  "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,
  "timestamp": "2026-05-25T11:17:26Z",
  "error_id": "45ed35e5-986c-4496-afd8-9a84f6baa410"
}
```

<Note>`discovered_at` is included for backwards compatibility and equals `completed_at`. Prefer `completed_at` — `discovered_at` will be removed in a future version.</Note>

### Possible failure codes

The `code`, `message`, and `errors[]` below are spread directly into the `radar_failure` envelope above — identical to what the sync single-create endpoint returns for the same failure. Your existing sync error handling works for bulk without changes.

<AccordionGroup>
  <Accordion title="400 — Validation error">
    Returned when a field is missing, invalid, or the radar type is not supported. `errors[]` may contain multiple entries, one per failing field.

    ```json theme={null}
    {
      "status": "error",
      "code": 400,
      "message": "Validation error",
      "errors": [
        {
          "field": "domain",
          "reason": "Invalid domain format"
        }
      ],
      "timestamp": "2026-05-25T11:17:26Z",
      "error_id": "245df4c2-3ab8-4c96-a60d-42f6fc59f9b8"
    }
    ```
  </Accordion>

  <Accordion title="402 — Insufficient balance">
    Returned when your account does not have enough credits to create the radar. The item is not created and no credits are deducted.

    ```json theme={null}
    {
      "status": "error",
      "code": 402,
      "message": "Insufficient balance",
      "errors": [
        {
          "field": "balance",
          "reason": "You do not have enough balance to create this radar."
        }
      ],
      "timestamp": "2026-05-25T11:17:26Z",
      "error_id": "29a4ff58-f4a8-4ebe-8b2d-1544ee43a99c"
    }
    ```
  </Accordion>

  <Accordion title="409 — Conflict (radar already exists)">
    Returned when a radar with the same configuration already exists on your account. Always includes `conflicting_radar_id` so you can look it up directly.

    The `field` name varies by radar type:

    * **Company** radars → `domain`
    * **Contact** radars → `identifiers`
    * **Industry** radars → `keyword` (for `industry_mentions` / `industry_job_openings`) or `radar_type` (for other industry types)

    ```json theme={null}
    {
      "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"
        }
      ],
      "timestamp": "2026-05-25T11:17:26Z",
      "error_id": "45ed35e5-986c-4496-afd8-9a84f6baa410"
    }
    ```
  </Accordion>

  <Accordion title="500 — Internal server error">
    Rare. Indicates a transient infrastructure failure. The item will be retried automatically — no action needed on your side. If a `radar_failure` with `code: 500` is delivered, it means all retries were exhausted.

    ```json theme={null}
    {
      "status": "error",
      "code": 500,
      "message": "Internal server error",
      "errors": [
        {
          "field": "request",
          "reason": "Create failed"
        }
      ],
      "timestamp": "2026-05-25T11:17:26Z",
      "error_id": "f9ab0e2d-8246-4465-868f-922add7fbeab"
    }
    ```
  </Accordion>
</AccordionGroup>

## Step 5: Finalize on `bulk_completed`

When all items reach terminal state, you receive one `bulk_completed` event. Use it to:

* mark the bulk job complete in your system
* compare final counts (`created`, `failed`) with your local ledger
* trigger any retry workflow for failed items

<Warning>
  `bulk_completed` carries the full per-item result array. At 1,000 items with rich `custom_fields` or filter data, the payload can approach 1–2 MB — ensure your receiver can handle it. Alternatively, use `GET /v1/radars/bulk/{bulk_id}` to fetch results on demand instead of relying on this webhook.
</Warning>

`bulk_completed.radars[]` contains per-item results for the submission.
Each `radars[]` entry is identical to the per-item webhook payload already delivered for that item (`radar_created` or `radar_failure`).

```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": 2,
    "created": 1,
    "failed": 1
  },
  "radars": [
    {
      "update_type": "radar_created",
      "item_index": 0,
      "...": "same fields as radar_created (including custom_fields and data)"
    },
    {
      "update_type": "radar_failure",
      "item_index": 1,
      "...": "same fields as radar_failure (including code, errors, error_id, timestamp)"
    }
  ]
}
```

## Step 6: Add polling as a reliability path

If webhook delivery is delayed or your consumer is unavailable, poll:

`GET /v1/radars/bulk/{bulk_id}`

The response `status` is:

* `processing`: at least one item is still in flight
* `completed`: all items are terminal

Poll until `completed`, then stop.

Both polling responses include `radars[]`:

* In `processing`, in-flight items appear as `{ item_index, status: "processing", custom_fields }`.
* In `completed`, each item is the full terminal payload (same shape as per-item webhook delivery).

```bash theme={null}
curl https://api.tamradar.com/v1/radars/bulk/5f9da22e-4778-427c-b7e4-dab2a8d9d709 \
  -H "x-api-key: YOUR_API_KEY"
```

`processing` example:

```json theme={null}
{
  "status": "processing",
  "code": 200,
  "timestamp": "2026-05-25T12:19:17Z",
  "message": "Bulk submission is being processed",
  "bulk_id": "5d089cd9-239d-4de4-af1d-8d19da889d00",
  "summary": {
    "total": 3,
    "processing": 3,
    "created": 0,
    "failed": 0
  },
  "radars": [
    {
      "item_index": 0,
      "status": "processing",
      "custom_fields": {}
    },
    {
      "item_index": 1,
      "status": "processing",
      "custom_fields": {}
    },
    {
      "item_index": 2,
      "status": "processing",
      "custom_fields": {}
    }
  ]
}
```

`completed` example:

```json theme={null}
{
  "status": "completed",
  "code": 200,
  "timestamp": "2026-05-25T12:19:46Z",
  "message": "Bulk submission completed",
  "bulk_id": "5d089cd9-239d-4de4-af1d-8d19da889d00",
  "summary": {
    "total": 3,
    "processing": 0,
    "created": 0,
    "failed": 3
  },
  "radars": [
    {
      "item_index": 0,
      "custom_fields": {},
      "code": 409,
      "errors": [
        {
          "field": "domain",
          "reason": "A radar with this domain and type already exists for your account. Conflicting radar_id: 351d5644-3d75-416e-b8a5-eb0e1b16b602. You can view it using GET /radars/{radar_id}",
          "conflicting_radar_id": "351d5644-3d75-416e-b8a5-eb0e1b16b602"
        }
      ],
      "status": "error",
      "message": "Conflict: Radar already exists",
      "error_id": "f9ab0e2d-8246-4465-868f-922add7fbeab",
      "timestamp": "2026-05-25T12:19:45.783Z"
    },
    {
      "...": "additional items with the same terminal shape"
    }
  ]
}
```

## Recommended production pattern

1. Submit bulk and store `bulk_id`.
2. Process webhook events and persist item outcomes by `bulk_id + item_index`.
3. Poll status endpoint on a fallback interval until completed.
4. Reconcile webhook-driven and poll-driven outcomes.
5. Retry only failed items with a new bulk request.

This gives you real-time processing through webhooks and deterministic recovery through polling.
