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

# Create an ad group

> Generate a group of chat-card ads that share a landing page, price and targeting.

[← ChatGPT ads](/chatgpt-ads/overview)

**Endpoint:** `POST https://makelocalads.com/api/v1/chatgpt-ad-groups`
**Permission:** `chatgpt_ads:generate`

<Card title="Open in LocalAds" icon="arrow-up-right" href="https://makelocalads.com/app/a1516a75-9be5-461a-93af-c1896a0a3127?tab=studio&type=chatgpt-ads">
  The ChatGPT Ads workspace in the product's studio.
</Card>

An ad group holds chat-card ads that share a landing page, price and
direction. The landing page is required; the direction (`angle`) and price are
optional. Each ad gets an image, a title and a body line written for that
direction.

## Usage notes

* `target_url` is the landing page every ad in the group links to. `angle`
  also names the group.
* `ad_count` can be `1`, `3`, `5`, or `8`. Each ad costs 1 credit.
* `max_credits` refuses the request with `422` before anything is charged if
  the group would cost more.
* Send an `Idempotency-Key` so a retry after a network failure never starts
  and charges for a second group.

## Quick start

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --request POST \
    --url https://makelocalads.com/api/v1/chatgpt-ad-groups \
    --header "Authorization: Bearer $LOCALADS_API_KEY" \
    --header "Content-Type: application/json" \
    --header "Idempotency-Key: chatgpt-group-77" \
    --data '{
      "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
      "target_url": "https://shop.example.com/products/linen-throw",
      "angle": "The gift for people who have everything",
      "price": "$89",
      "ad_count": 3
    }'
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const response = await fetch(
    "https://makelocalads.com/api/v1/chatgpt-ad-groups",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.LOCALADS_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": "chatgpt-group-77",
      },
      body: JSON.stringify({
        product_id: "a1516a75-9be5-461a-93af-c1896a0a3127",
        target_url: "https://shop.example.com/products/linen-throw",
        angle: "The gift for people who have everything",
        price: "$89",
        ad_count: 3,
      }),
    },
  );

  const group = await response.json();
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import os
  import requests

  response = requests.post(
      "https://makelocalads.com/api/v1/chatgpt-ad-groups",
      headers={
          "Authorization": f"Bearer {os.environ['LOCALADS_API_KEY']}",
          "Content-Type": "application/json",
          "Idempotency-Key": "chatgpt-group-77",
      },
      json={
          "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
          "target_url": "https://shop.example.com/products/linen-throw",
          "angle": "The gift for people who have everything",
          "price": "$89",
          "ad_count": 3,
      },
  )

  group = response.json()
  ```
</CodeGroup>

## Input fields

<ParamField body="product_id" type="string" required>
  The product the ads are for.
</ParamField>

<ParamField body="target_url" type="string" required>
  The landing page every ad in the group links to, as a URL, up to 2,048
  characters.
</ParamField>

<ParamField body="angle" type="string">
  Optional direction for the ads, up to 2,000 characters. It also names the
  group.
</ParamField>

<ParamField body="price" type="string">
  The product's price shown in the ads, up to 100 characters, such as `$89`.
  Optional.
</ParamField>

<ParamField body="ad_count" type="integer">
  How many ads to generate: `1`, `3`, `5`, or `8`. Defaults to `3`.
</ParamField>

<ParamField body="max_credits" type="integer">
  Refuse the request with `422 credit_limit_exceeded`, before anything is
  charged, if it would cost more than this many credits.
</ParamField>

<Accordion title="Complete JSON schema">
  ```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "type": "object",
    "additionalProperties": false,
    "required": ["product_id", "target_url"],
    "properties": {
      "product_id": { "type": "string", "format": "uuid" },
      "target_url": { "type": "string", "format": "uri", "maxLength": 2048 },
      "angle": { "type": "string", "minLength": 1, "maxLength": 2000 },
      "price": { "type": "string", "minLength": 1, "maxLength": 100 },
      "ad_count": { "type": "integer", "enum": [1, 3, 5, 8], "default": 3 },
      "max_credits": { "type": "integer", "minimum": 1 }
    }
  }
  ```
</Accordion>

## Response

`202 Accepted` with the group, its `id` and one `ads` entry per requested ad,
all `queued`:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "2b8f6d4e-1a3c-4e5f-9b7d-0c2e4f6a8b1d",
  "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
  "name": "Linen throw: The gift for people who have everything",
  "target_url": "https://shop.example.com/products/linen-throw",
  "angle": "The gift for people who have everything",
  "price": "$89",
  "context_hints": [],
  "negative_phrases": [],
  "status": "in_progress",
  "credits_charged": 3,
  "ads": [
    {
      "id": "8e1c3a5b-7d9f-4b2e-a6c8-0d2f4e6a8c1b",
      "status": "queued",
      "position": 0,
      "title": null,
      "body": null,
      "image_url": null,
      "width": null,
      "height": null
    }
  ],
  "created_at": "2026-09-25T10:00:00.000Z",
  "updated_at": "2026-09-25T10:00:00.000Z"
}
```

Poll `GET /chatgpt-ad-groups/{ad_group_id}` with backoff until `status` is
`completed` or `failed`; see [Polling](/concepts/polling). `title`, `body`,
`context_hints` and `negative_phrases` are filled in once the copy is written.

A completed group looks like this:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "2b8f6d4e-1a3c-4e5f-9b7d-0c2e4f6a8b1d",
  "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
  "name": "Linen throw: The gift for people who have everything",
  "target_url": "https://shop.example.com/products/linen-throw",
  "angle": "The gift for people who have everything",
  "price": "$89",
  "context_hints": ["thoughtful housewarming gifts", "cozy home upgrades", "gifts for parents"],
  "negative_phrases": ["free", "cheap blanket"],
  "status": "completed",
  "credits_charged": 3,
  "ads": [
    {
      "id": "8e1c3a5b-7d9f-4b2e-a6c8-0d2f4e6a8c1b",
      "status": "completed",
      "position": 0,
      "title": "Gift they will use daily",
      "body": "Stonewashed linen, softer every wash.",
      "image_url": "https://cdn.example.com/chatgpt/throw-1.png",
      "width": 1024,
      "height": 1024
    }
  ],
  "created_at": "2026-09-25T10:00:00.000Z",
  "updated_at": "2026-09-25T10:00:00.000Z"
}
```

## Errors

| Status | Code | Meaning |
| - | - | - |
| `404` | `product_not_found` | The product is not available to you |
| `422` | `insufficient_credits` | Not enough credits for the batch. Nothing was charged |
| `422` | `credit_limit_exceeded` | The batch would cost more than `max_credits` |
| `422` | `plan_required` | Your plan does not include generation |

See [Errors and retries](/concepts/errors) for the error format.
