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

# Geographic Targeting

> Target campaigns by country with targeting.locations.include — country-level only, no region or DMA targeting.

Thrad campaigns are targeted **by country**. You set targeting on the campaign at create time (or update it later) through `targeting.locations.include`, an array of country objects. There is no separate targeting resource — targeting lives on the campaign body.

<Note>
  Targeting is **country-level only**. Region, state, and DMA (metro) targeting are **not supported**. This is a deliberate difference from OpenAI Ads — locations are resolved to a country and any sub-country detail is ignored.
</Note>

## Authentication

All requests use your organization's API key.

<ParamField header="Authorization" type="string" required>
  Bearer token: `Authorization: Bearer ak_...`. One active key per organization — create it in the Thrad Platform dashboard under **Settings → API keys**. Examples below read it from the `THRAD_ADS_API_KEY` environment variable.
</ParamField>

## The targeting object

<ParamField body="targeting" type="object">
  Geographic targeting for the campaign.

  <Expandable title="targeting properties">
    <ParamField body="locations" type="object">
      Location targeting container.

      <Expandable title="locations properties">
        <ParamField body="include" type="array">
          Countries to target. Each entry is a country object. Omit `targeting` (or send an empty `include`) to leave the campaign untargeted by geography.

          <Expandable title="include[] properties">
            <ParamField body="country_code" type="string" required>
              ISO 3166-1 alpha-2 country code, e.g. `"US"`, `"DE"`, `"GB"`. Case-insensitive on the way in — codes are uppercased server-side. As an alias, you may send `id` instead of `country_code` (e.g. `{ "id": "us" }`); the value is read from `country_code` first, then `id`.
            </ParamField>
          </Expandable>
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<Warning>
  Every location in `include` must resolve to a country code. An entry with neither `country_code` nor `id` is rejected with `400 invalid_value` and `param: "targeting.locations.include"`.
</Warning>

## Setting targeting on create

Pass `targeting` inside the campaign create body. Country codes are uppercased and normalized before the campaign is stored.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thrad.ai/v1/campaigns \
    -H "Authorization: Bearer $THRAD_ADS_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: 9f1c2b7a-0e3d-4a51-8c6e-1d2f3a4b5c6d" \
    -d '{
      "name": "US + Germany launch",
      "budget": { "lifetime_spend_limit_micros": 50000000 },
      "targeting": {
        "locations": {
          "include": [
            { "country_code": "US" },
            { "country_code": "de" }
          ]
        }
      },
      "ad_groups": [
        {
          "name": "Prospecting",
          "bidding_config": { "max_bid_micros": 2000000 },
          "ads": [
            {
              "name": "Card A",
              "creative": {
                "type": "chat_card",
                "title": "Try our app",
                "body": "Fast, simple, free to start.",
                "target_url": "https://example.com",
                "image_url": "https://cdn.example.com/card.jpg"
              }
            }
          ]
        }
      ]
    }'
  ```

  ```python Python theme={null}
  import os
  import requests

  resp = requests.post(
      "https://api.thrad.ai/v1/campaigns",
      headers={
          "Authorization": f"Bearer {os.environ['THRAD_ADS_API_KEY']}",
          "Content-Type": "application/json",
          "Idempotency-Key": "9f1c2b7a-0e3d-4a51-8c6e-1d2f3a4b5c6d",
      },
      json={
          "name": "US + Germany launch",
          "budget": {"lifetime_spend_limit_micros": 50_000_000},
          "targeting": {
              "locations": {
                  "include": [
                      {"country_code": "US"},
                      {"country_code": "de"},
                  ]
              }
          },
          "ad_groups": [
              {
                  "name": "Prospecting",
                  "bidding_config": {"max_bid_micros": 2_000_000},
                  "ads": [
                      {
                          "name": "Card A",
                          "creative": {
                              "type": "chat_card",
                              "title": "Try our app",
                              "body": "Fast, simple, free to start.",
                              "target_url": "https://example.com",
                              "image_url": "https://cdn.example.com/card.jpg",
                          },
                      }
                  ],
              }
          ],
      },
  )
  resp.raise_for_status()
  print(resp.json())
  ```
</CodeGroup>

`budget.lifetime_spend_limit_micros` is in **integer micros** (`$1 = 1,000,000`); `50000000` is `$50.00`. `max_bid_micros` is likewise micros (`2000000` = `$2.00`).

## Read-back shape

On read, each targeted country is expanded to an object with `id`, `type`, and `country_code` — `type` is always `"country"`, and `id` mirrors the uppercased country code. The campaign you created above reads back like this:

<ResponseExample>
  ```json 200 OK theme={null}
  {
    "id": "c4d5e6f7-1234-4abc-9def-0123456789ab",
    "name": "US + Germany launch",
    "description": null,
    "status": "paused",
    "review_status": "in_review",
    "start_time": 1751414400,
    "end_time": null,
    "budget": {
      "lifetime_spend_limit_micros": 50000000
    },
    "recurring_frequency": "daily",
    "pricing_model": "cpm",
    "targeting": {
      "locations": {
        "include": [
          { "id": "US", "type": "country", "country_code": "US" },
          { "id": "DE", "type": "country", "country_code": "DE" }
        ]
      }
    },
    "created_at": 1751241600,
    "updated_at": 1751241600
  }
  ```
</ResponseExample>

<Note>
  The lowercase `"de"` you sent reads back as `"DE"`. Timestamps (`start_time`, `end_time`, `created_at`, `updated_at`) are Unix seconds. A freshly created campaign submits to review, so it reads back as `status: "paused"` with `review_status: "in_review"`.

  You omitted `start_time` on create, so it defaulted to roughly two days out (the earliest allowed launch window) — that is why `start_time` reads back as a non-null Unix timestamp rather than `null`. `end_time` was also omitted and stays `null` (open-ended, delivering until the budget is exhausted).
</Note>

## Updating targeting

Send a new `targeting` object to `POST /v1/campaigns/{id}` to replace the campaign's countries. The same normalization and validation apply on update as on create.

```bash cURL theme={null}
curl https://api.thrad.ai/v1/campaigns/c4d5e6f7-1234-4abc-9def-0123456789ab \
  -H "Authorization: Bearer $THRAD_ADS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "targeting": {
      "locations": {
        "include": [
          { "country_code": "US" }
        ]
      }
    }
  }'
```

<Warning>
  Do not send `status` in a **campaign** update body — campaign status changes go only through `/activate`, `/pause`, and `/archive`. Sending `status` on a campaign update returns `400 use_status_action`. (Ad groups and ads do accept `status` in their update body — that restriction is campaign-only.)
</Warning>

## Errors

Errors use the bare error shape:

```json theme={null}
{
  "error": {
    "message": "Each targeting location needs a country_code.",
    "type": "invalid_request_error",
    "param": "targeting.locations.include",
    "code": "invalid_value"
  }
}
```

| HTTP  | `code`              | When                                                                                            |
| ----- | ------------------- | ----------------------------------------------------------------------------------------------- |
| `400` | `invalid_value`     | A location entry has neither `country_code` nor `id`. `param` is `targeting.locations.include`. |
| `400` | `use_status_action` | `status` was sent in a **campaign** update body. Use `/activate`, `/pause`, or `/archive`.      |
| `401` | `auth_required`     | No `Authorization` header was sent.                                                             |
| `401` | `invalid_api_key`   | The `Authorization: Bearer ak_...` key is malformed, unknown, or revoked.                       |
| `404` | `not_found`         | The campaign `{id}` does not exist for your organization.                                       |

## Tips

<Tip>
  Create the campaign, then open it in the Thrad Platform dashboard and confirm the targeted countries before it goes live. The campaign starts in review (`review_status: "in_review"`) and will not deliver until it is approved, so this is the right moment to catch a wrong or missing country.
</Tip>

<Tip>
  If you need finer-grained delivery than country level, that is not available through targeting. Region, state, and DMA targeting are intentionally unsupported — design your campaigns around country-level reach.
</Tip>
