> ## 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 a campaign

> Start a guided or prompt campaign and retrieve its creatives.

[← Ad campaigns](/ad-campaigns/overview)

**Endpoint:** `POST https://makelocalads.com/api/v1/campaigns`
**Permission:** `campaigns:generate`

<Card title="Open in LocalAds" icon="arrow-up-right" href="https://makelocalads.com/app/a1516a75-9be5-461a-93af-c1896a0a3127?tab=studio&type=ad-campaigns-v2">
  The campaign creator in the product's Ad Campaign studio.
</Card>

## Usage notes

* Guided mode (the default) requires `audience_id`: a saved audience of the
  product, or `null` for a deliberately broad campaign. Leaving it out returns
  `400 invalid_request`, so an audience is never skipped by accident. See
  [Audiences](/setup/audiences).
* Prompt mode takes a `brief` instead: it is the whole direction, with no
  audience step.
* Each creative costs 2 credits. `max_credits` refuses the request with `422`
  before anything is charged if the campaign would cost more.
* Send an `Idempotency-Key` so a retry after a network failure never starts
  and charges for a second campaign.

## Quick start

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --request POST \
    --url https://makelocalads.com/api/v1/campaigns \
    --header "Authorization: Bearer $LOCALADS_API_KEY" \
    --header "Content-Type: application/json" \
    --header "Idempotency-Key: campaign-order-1042" \
    --data '{
      "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
      "blueprint_id": "viral-pattern-interrupt",
      "audience_id": "0f0c5a4e-54f7-4f0e-9d4e-1a8c3a6b5d11",
      "angles": ["Setup takes half the time"],
      "creative_count": 10,
      "max_credits": 20
    }'
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const response = await fetch("https://makelocalads.com/api/v1/campaigns", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LOCALADS_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": "campaign-order-1042",
    },
    body: JSON.stringify({
      product_id: "a1516a75-9be5-461a-93af-c1896a0a3127",
      blueprint_id: "viral-pattern-interrupt",
      audience_id: "0f0c5a4e-54f7-4f0e-9d4e-1a8c3a6b5d11",
      angles: ["Setup takes half the time"],
      creative_count: 10,
      max_credits: 20,
    }),
  });

  const campaign = 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/campaigns",
      headers={
          "Authorization": f"Bearer {os.environ['LOCALADS_API_KEY']}",
          "Content-Type": "application/json",
          "Idempotency-Key": "campaign-order-1042",
      },
      json={
          "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
          "blueprint_id": "viral-pattern-interrupt",
          "audience_id": "0f0c5a4e-54f7-4f0e-9d4e-1a8c3a6b5d11",
          "angles": ["Setup takes half the time"],
          "creative_count": 10,
          "max_credits": 20,
      },
  )

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

## The choices

| Field | Values | Default |
| - | - | - |
| `mode` | `guided` or `prompt` | `guided` |
| `blueprint_id` | one recipe id | the creator's default recipe |
| `blueprint_ids` | several ad types (a custom campaign) | none |
| `audience_id` | required in guided mode: a saved audience of the product, or `null` for broad | none |
| `angles` | up to 10, single recipe only | none |
| `brief` | up to 2,000 characters; required in prompt mode | none |
| `aspect_ratio` | `1:1`, `4:5`, `9:16`, `16:9` | `1:1` |
| `language` | one of the supported languages | the brand's saved language |
| `creative_count` | `5`, `10`, `15`, `20` | `10` |
| `max_credits` | refuse if the campaign would cost more | none |

A custom campaign (`blueprint_ids`) splits the creatives evenly across the ad
types, so `creative_count` must be a multiple of the number of types, up to
30\. Angles apply to a single recipe, so they cannot be combined with
`blueprint_ids`.

To describe the campaign yourself instead:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
  "mode": "prompt",
  "brief": "A premium campaign on the craftsmanship and gift appeal.",
  "creative_count": 10
}
```

<Note>
  The original `prompt` field still works: it means prompt mode with that brief
  and keeps the original counts (3 to 30). New integrations should send
  `mode: "prompt"` and `brief`.
</Note>

## Input fields

<ParamField body="product_id" type="string" required>
  The product the campaign is for, with `status: "ready"`.
</ParamField>

<ParamField body="mode" type="string">
  `guided` (the default) starts with the audience, then a recipe or ad types.
  `prompt` takes a `brief` and has no audience step.
</ParamField>

<ParamField body="blueprint_id" type="string">
  One recipe from `GET /campaign-blueprints`. Optional; without a recipe or ad
  types, one is chosen for the product.
</ParamField>

<ParamField body="blueprint_ids" type="array">
  Several ad types for a custom campaign, 1 to 30 unique recipe ids. Creatives
  are split evenly across them, so `creative_count` must be a multiple of the
  number of types. Not allowed with `angles`.
</ParamField>

<ParamField body="audience_id" type="string">
  Required in guided mode. A saved audience of the product, from
  `GET /products/{product_id}/audiences`, or `null` for a broad campaign.
  Prompt mode takes no audience: omit it or send `null`.
</ParamField>

<ParamField body="angles" type="array">
  Angles for a single recipe, usually from
  [Recipes and angles](/ad-campaigns/recipes-and-angles). 1 to 10 strings, up
  to 300 characters each. Not allowed with `blueprint_ids`.
</ParamField>

<ParamField body="brief" type="string">
  Optional in guided mode, up to 2,000 characters. Required in prompt mode,
  where it is the whole direction.
</ParamField>

<ParamField body="prompt" type="string">
  Deprecated. The original prompt-only field: sending it means prompt mode with
  this brief, and keeps the original counts (3 to 30). Use `mode: "prompt"` and
  `brief` instead.
</ParamField>

<ParamField body="aspect_ratio" type="string">
  One of `1:1`, `4:5`, `9:16`, `16:9`. Defaults to `1:1`.
</ParamField>

<ParamField body="language" type="string">
  One of the 42 supported languages, such as `English`, `Hindi`, `Spanish`.
  Defaults to the brand's saved language, else English.
</ParamField>

<ParamField body="creative_count" type="integer">
  How many creatives to make: `5`, `10`, `15`, or `20` (the default). For
  `blueprint_ids`, a multiple of the number of ad types, up to 30 (the default
  is the multiple nearest 10).
</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"],
    "properties": {
      "product_id": { "type": "string", "format": "uuid" },
      "mode": { "type": "string", "enum": ["guided", "prompt"], "default": "guided" },
      "blueprint_id": { "type": "string" },
      "blueprint_ids": {
        "type": "array",
        "minItems": 1,
        "maxItems": 30,
        "uniqueItems": true,
        "items": { "type": "string" }
      },
      "audience_id": { "type": ["string", "null"], "format": "uuid" },
      "angles": {
        "type": "array",
        "minItems": 1,
        "maxItems": 10,
        "items": { "type": "string", "maxLength": 300 }
      },
      "brief": { "type": "string", "minLength": 1, "maxLength": 2000 },
      "prompt": { "type": "string", "minLength": 1, "maxLength": 2000, "deprecated": true },
      "aspect_ratio": { "type": "string", "enum": ["1:1", "4:5", "9:16", "16:9"], "default": "1:1" },
      "language": { "type": "string" },
      "creative_count": { "type": "integer" },
      "max_credits": { "type": "integer", "minimum": 1 }
    }
  }
  ```
</Accordion>

## Response

LocalAds returns `202 Accepted` and a `Location` header:

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
Location: /api/v1/campaigns/84b92447-425f-4d2a-9ba1-7d67bf802332
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "84b92447-425f-4d2a-9ba1-7d67bf802332",
  "brand_id": "1423f915-beca-4c53-a5e4-7c8c99537be9",
  "product_id": "a1516a75-9be5-461a-93af-c1896a0a3127",
  "name": "Viral pattern interrupt",
  "status": "queued",
  "credits_charged": 20,
  "requested_creative_count": 10,
  "completed_creative_count": 0,
  "failed_creative_count": 0,
  "creatives": [
    {
      "id": "19025a65-5395-41c9-af90-d7cca7fb6421",
      "status": "queued",
      "image_url": null,
      "width": null,
      "height": null
    }
  ],
  "created_at": "2026-09-25T10:00:00.000Z",
  "updated_at": "2026-09-25T10:00:00.000Z"
}
```

Poll the URL in the `Location` header until `status` is `completed`, `failed`,
or `canceled`, with backoff between requests. See
[Polling](/concepts/polling).

Completed creatives appear while the rest continue generating. A `completed`
campaign may contain an individually failed creative. Its credits are refunded
and `credits_charged` goes down to match. Check `failed_creative_count` and
each creative's status before using the results.

## Errors

| Status | Code | Meaning |
| - | - | - |
| `400` | `invalid_request` | `audience_id` is missing from a guided campaign (send an audience or `null`) |
| `400` | `invalid_request` | A choice the creator does not offer, such as `creative_count: 7` or angles with `blueprint_ids` |
| `404` | `product_not_found`, `blueprint_not_found`, `audience_not_found` | The product, recipe or audience is not available to you |
| `422` | `insufficient_credits` | Not enough credits for every creative. Nothing was charged |
| `422` | `credit_limit_exceeded` | The campaign would cost more than `max_credits`. Nothing was charged |
| `422` | `key_budget_exceeded` | This API key has used its monthly budget |

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